diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go index ca1bc536d90..ca7ceb21082 100644 --- a/go/analysis/passes/fieldalignment/fieldalignment.go +++ b/go/analysis/passes/fieldalignment/fieldalignment.go @@ -79,6 +79,7 @@ func fieldalignment(pass *analysis.Pass, node *ast.StructType, typ *types.Struct // TODO: Preserve comment, for now get rid of them. // See https://github.com/golang/go/issues/20744 f.Comment = nil + f.Doc = nil if len(f.Names) <= 1 { flat = append(flat, f) continue diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a.go b/go/analysis/passes/fieldalignment/testdata/src/a/a.go index b47ee19c03e..463b4cb18f0 100644 --- a/go/analysis/passes/fieldalignment/testdata/src/a/a.go +++ b/go/analysis/passes/fieldalignment/testdata/src/a/a.go @@ -35,3 +35,12 @@ type NoNameBad struct { // want "struct of size 20 could be 16" y int32 z byte } + +type WithComments struct { // want "struct of size 8 could be 4" + // doc style comment + a uint32 // field a comment + b [0]byte // field b comment + // other doc style comment + + // and a last comment +} diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden b/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden index 34fc21b9fd6..c1c75e2a123 100644 --- a/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden +++ b/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden @@ -35,3 +35,8 @@ type NoNameBad struct { x byte z byte } + +type WithComments struct { + b [0]byte + a uint32 +} diff --git a/go/packages/golist.go b/go/packages/golist.go index ec417ba830e..f89b05bb149 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -10,7 +10,6 @@ import ( "encoding/json" "fmt" "go/types" - exec "golang.org/x/sys/execabs" "io/ioutil" "log" "os" @@ -23,6 +22,7 @@ import ( "sync" "unicode" + exec "golang.org/x/sys/execabs" "golang.org/x/tools/go/internal/packagesdriver" "golang.org/x/tools/internal/gocommand" "golang.org/x/xerrors" @@ -865,7 +865,7 @@ func (state *golistState) invokeGo(verb string, args ...string) (*bytes.Buffer, if gocmdRunner == nil { gocmdRunner = &gocommand.Runner{} } - stdout, stderr, _, err := gocmdRunner.RunRaw(cfg.Context, inv) + stdout, stderr, friendlyErr, err := gocmdRunner.RunRaw(cfg.Context, inv) if err != nil { // Check for 'go' executable not being found. if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound { @@ -886,7 +886,7 @@ func (state *golistState) invokeGo(verb string, args ...string) (*bytes.Buffer, // Related to #24854 if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "unexpected directory layout") { - return nil, fmt.Errorf("%s", stderr.String()) + return nil, friendlyErr } // Is there an error running the C compiler in cgo? This will be reported in the "Error" field @@ -999,7 +999,7 @@ func (state *golistState) invokeGo(verb string, args ...string) (*bytes.Buffer, // TODO(matloob): Remove these once we can depend on go list to exit with a zero status with -e even when // packages don't exist or a build fails. if !usesExportData(cfg) && !containsGoFile(args) { - return nil, fmt.Errorf("go %v: %s: %s", args, exitErr, stderr) + return nil, friendlyErr } } return stdout, nil diff --git a/gopls/README.md b/gopls/README.md index d2baf728852..18798e1ae3b 100644 --- a/gopls/README.md +++ b/gopls/README.md @@ -1,73 +1,99 @@ -# gopls documentation +# `gopls`, the Go language server [![PkgGoDev](https://pkg.go.dev/badge/golang.org/x/tools/gopls)](https://pkg.go.dev/golang.org/x/tools/gopls) -gopls (pronounced: "go please") is the official [language server] for the Go language. +`gopls` (pronounced "Go please") is the official Go [language server] developed +by the Go team. It provides IDE features to any [LSP]-compatible editor. -## Status + -It is currently in **alpha**, so it is **not stable**. +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. -You can see more information about the status of gopls and its supported features [here](doc/status.md). +## Editors -## Roadmap +To get started with `gopls`, install an LSP plugin in your editor of choice. -The current goal is a fully stable build with the existing feature set, aiming -for the first half of 2020, with release candidates earlier in the year. +* [VSCode](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) -This will be the first build that we recommend people use, and will be tagged as the 1.0 version. -You can see the set of things being worked on in the [1.0 milestone], in general -we are focused on stability, specifically, making sure we have a reliable service that produces an experience in module mode that is not a retrograde step from the old tools in GOPATH mode. +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). -There is also considerable effort being put into testing in order to make sure that we both have a stable service and also that we do not regress after launch. +## Installation -While we may continue to accept contributions for new features, they may be turned off behind a configuration flag if they are not yet stable. See the [gopls unplanned] milestone for deprioritized features. +For the most part, you should not need to install or update `gopls`. Your +editor should handle that step for you. -This is just a milestone for gopls itself. We work with editor integrators to make sure they can use the latest builds of gopls, and will help them use the 1.0 version as soon as it is ready, but that does not imply anything about the stability, supported features or version of the plugins. +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: -## Using +```sh +GO111MODULE=on go get golang.org/x/tools/gopls@latest +``` -In general you should not need to know anything about gopls, it should be integrated into your editor for you. +**NOTE**: Do not use the `-u` flag, as it will update your dependencies to +incompatible versions. -To install for your specific editor you can follow the following instructions +Learn more in the [advanced installation +instructions](doc/advanced.md#installing-unreleased-versions). -* [VSCode](doc/vscode.md) -* [Vim / Neovim](doc/vim.md) -* [Emacs](doc/emacs.md) -* [Acme](doc/acme.md) -* [Sublime Text](doc/subl.md) -* [Atom](doc/atom.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. + +## 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). -See the [user guide](doc/user.md) for more information, including the how to install gopls by hand if you need. +### Environment variables -## Issues +`gopls` inherits your editor's environment, so be aware of any environment +variables you configure. Some editors, such as VS Code, allow users to +selectively override the values of some environment variables. -If you are having issues with gopls, please first check the [known issues](doc/status.md#known-issues) before following the [troubleshooting](doc/troubleshooting.md#steps) guide. -If that does not give you the information you need, reach out to us. +## Troubleshooting -You can chat with us on: -* the golang-tools [mailing list] -* the #gopls [slack channel] on the gophers slack +If you are having issues with `gopls`, please follow the steps described in the +[troubleshooting guide](doc/troubleshooting.md). -If you think you have an issue that needs fixing, or a feature suggestion, then please make sure you follow the steps to [file an issue](doc/troubleshooting.md#file-an-issue) with the right information to allow us to address it. +## Supported Go versions and build systems -If you need to talk to us directly (for instance to file an issue with confidential information in it) you can reach out directly to [@stamblerre] or [@ianthehat]. +`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. Though we +try not to break older versions, we do not prioritize issues only affecting +legacy Go releases. -## More information +`gopls` currently only supports the `go` command, so if you are using a +different build system, `gopls` will not work well. Bazel support is currently +blocked on +[bazelbuild/rules_go#512](https://github.com/bazelbuild/rules_go/issues/512). -If you want to know more about it, have an unusual use case, or want to contribute, please read the following documents +## Additional information -* [Using gopls](doc/user.md) -* [Troubleshooting and reporting issues](doc/troubleshooting.md) -* [Integrating gopls with an editor](doc/integrating.md) -* [Contributing to gopls](doc/contributing.md) -* [Design requirements and decisions](doc/design.md) -* [Implementation details](doc/implementation.md) +* [Features](doc/features.md) +* [Command-line interface](doc/command-line.md) +* [Advanced topics](doc/advanced.md) +* [Contributing to `gopls`](doc/contributing.md) +* [Integrating `gopls` with an editor](doc/design/integrating.md) +* [Design requirements and decisions](doc/design/design.md) +* [Implementation details](doc/design/implementation.md) +* [Open issues](https://github.com/golang/go/issues?q=is%3Aissue+is%3Aopen+label%3Agopls) [language server]: https://langserver.org -[mailing list]: https://groups.google.com/forum/#!forum/golang-tools -[slack channel]: https://gophers.slack.com/messages/CJZH85XCZ -[@stamblerre]: https://github.com/stamblerre "Rebecca Stambler" -[@ianthehat]: https://github.com/ianthehat "Ian Cottrell" -[1.0 milestone]: https://github.com/golang/go/milestone/112 -[gopls unplanned]: https://github.com/golang/go/milestone/124 +[LSP]: https://microsoft.github.io/language-server-protocol/ +[Gophers Slack]: https://gophers.slack.com/ diff --git a/gopls/doc/acme.md b/gopls/doc/acme.md deleted file mode 100644 index e62a0286ec5..00000000000 --- a/gopls/doc/acme.md +++ /dev/null @@ -1,7 +0,0 @@ -# Acme - -Use the experimental [`acme-lsp`] plugin. -Get started by following the[installation guide]. - -[`acme-lsp`]: https://github.com/fhs/acme-lsp -[installation guide]: https://github.com/fhs/acme-lsp#gopls diff --git a/gopls/doc/advanced.md b/gopls/doc/advanced.md new file mode 100644 index 00000000000..93c6b8fdaca --- /dev/null +++ b/gopls/doc/advanced.md @@ -0,0 +1,37 @@ +# Advanced topics + +This documentation is for advanced `gopls` users, who may want to test +unreleased versions or try out special features. + +## Installing unreleased versions + +To get a specific version of `gopls` (for example, to test a prerelease +version), run: + +```sh +GO111MODULE=on go get golang.org/x/tools/gopls@vX.Y.Z +``` + +Where `vX.Y.Z` is the desired version. + +### Unstable versions + +To update `gopls` to the latest **unstable** version, use: + +```sh +GO111MODULE=on go get golang.org/x/tools/gopls@master golang.org/x/tools@master +``` + +## Working on the Go source distribution + +If you are working on the [Go project] itself, the `go` command that `gopls` +invokes will have to correspond to the version of the source you are working +on. That is, if you have checked out the Go project to `$HOME/go`, your `go` +command should be the `$HOME/go/bin/go` executable that you built with +`make.bash` or equivalent. + +You can achieve this by adding the right version of `go` to your `PATH` +(`export PATH=$HOME/go/bin:$PATH` on Unix systems) or by configuring your +editor. + +[Go project]: https://go.googlesource.com/go diff --git a/gopls/doc/atom.md b/gopls/doc/atom.md deleted file mode 100644 index ce1d094d7a0..00000000000 --- a/gopls/doc/atom.md +++ /dev/null @@ -1,7 +0,0 @@ -# Atom - -Use the [`ide-gopls`] package. -You will also need to install the [`atom-ide-ui`] package. - -[`ide-gopls`]: https://github.com/MordFustang21/ide-gopls -[`atom-ide-ui`]: https://github.com/facebookarchive/atom-ide-ui diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index f545551b721..0fa1e8645ce 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -53,6 +53,12 @@ Identifier: `gopls.go_get_package` go_get_package runs `go get` to fetch a package. +### **Check for upgrades** +Identifier: `gopls.check_upgrades` + +check_upgrades checks for module upgrades. + + ### **Add dependency** Identifier: `gopls.add_dependency` diff --git a/gopls/doc/contributing.md b/gopls/doc/contributing.md index b285a30ebab..a99dc6ef255 100644 --- a/gopls/doc/contributing.md +++ b/gopls/doc/contributing.md @@ -1,7 +1,7 @@ # Documentation for contributors This documentation augments the general documentation for contributing to the -x/tools repository, described at the [repository root](../CONTRIBUTING.md). +x/tools repository, described at the [repository root](../../CONTRIBUTING.md). Contributions are welcome, but since development is so active, we request that you file an issue and claim it before starting to work on something. Otherwise, @@ -96,7 +96,7 @@ Furthermore, an additional "gopls-CI" pass will be run by _Kokoro_, which is a Jenkins-like Google infrastructure for running Dockerized tests. This allows us to run gopls tests in various environments that would be difficult to add to the TryBots. Notably, Kokoro runs tests on -[older Go versions](user.md#supported-go-versions) that are no longer supported +[older Go versions](../README.md#supported-go-versions) that are no longer supported by the TryBots. ## Debugging diff --git a/gopls/doc/daemon.md b/gopls/doc/daemon.md index ea9c4e2d2ca..f54099cd2fd 100644 --- a/gopls/doc/daemon.md +++ b/gopls/doc/daemon.md @@ -150,10 +150,10 @@ will use the same shared daemon. **Q: Why am I not saving as much memory as I expected when using a shared gopls?** -A: As described in [implementation.md](implementation.md), gopls has a concept -of view/session/cache. Each session and view map onto exactly one editor -session (because they contain things like edited but unsaved buffers). The -cache contains things that are independent of any editor session, and can +A: As described in [implementation.md](design/implementation.md), gopls has a +concept of view/session/cache. Each session and view map onto exactly one +editor session (because they contain things like edited but unsaved buffers). +The cache contains things that are independent of any editor session, and can therefore be shared. When, for example, three editor session are sharing a single gopls process, diff --git a/gopls/doc/emacs.md b/gopls/doc/emacs.md index 707dd900cb5..471dbf1d5c0 100644 --- a/gopls/doc/emacs.md +++ b/gopls/doc/emacs.md @@ -3,7 +3,7 @@ ## Installing `gopls` To use `gopls` with Emacs, you must first -[install the `gopls` binary](user.md#installation) and ensure that the directory +[install the `gopls` binary](../README.md#installation) and ensure that the directory containing the resulting binary (either `$(go env GOBIN)` or `$(go env GOPATH)/bin`) is in your `PATH`. diff --git a/gopls/doc/troubleshooting.md b/gopls/doc/troubleshooting.md index 58e5fa46716..121dd86f958 100644 --- a/gopls/doc/troubleshooting.md +++ b/gopls/doc/troubleshooting.md @@ -10,7 +10,7 @@ VS Code users should follow [their troubleshooting guide](https://github.com/gol 1. Verify that your project is in good shape by working with it outside of your editor. Running a command like `go build ./...` in the workspace directory will compile everything. For modules, `go mod tidy` is another good check, though it may modify your `go.mod`. 1. Check that your editor isn't showing any diagnostics that indicate a problem with your workspace. They may appear as diagnostics on a Go file's package declaration, diagnostics in a go.mod file, or as a status or progress message. Problems in the workspace configuration can cause many different symptoms. See the [workspace setup instructions](workspace.md) for help. -1. Make sure `gopls` is up to date by following the [installation instructions](user.md#installing), then [restarting gopls](#restart-gopls). +1. Make sure `gopls` is up to date by following the [installation instructions](../README.md#installation), then [restarting gopls](#restart-gopls). 1. Optionally, [ask for help](#ask-for-help) on Gophers Slack. 1. Finally, [report the issue](#file-an-issue) to the `gopls` developers. @@ -41,7 +41,7 @@ You may have to change your editor's configuration to pass a `-logfile` flag to To increase the level of detail in your logs, start `gopls` with the `-rpc.trace` flag. To start a debug server that will allow you to see profiles and memory usage, start `gopls` with `serve --debug=localhost:6060`. You will then be able to view debug information by navigating to `localhost:6060`. -If you are unsure of how to pass a flag to `gopls` through your editor, please see the [documentation for your editor](user.md#editors). +If you are unsure of how to pass a flag to `gopls` through your editor, please see the [documentation for your editor](../README.md#editors). ## Debug memory usage diff --git a/gopls/doc/user.md b/gopls/doc/user.md deleted file mode 100644 index fce4b9ba588..00000000000 --- a/gopls/doc/user.md +++ /dev/null @@ -1,165 +0,0 @@ -# User guide - -**If you're having issues with `gopls`, please see the -[troubleshooting guide](troubleshooting.md).** - -## Editors - -The following is the list of editors with known integrations for `gopls`. - -* [VSCode](vscode.md) -* [Vim / Neovim](vim.md) -* [Emacs](emacs.md) -* [Acme](acme.md) -* [Sublime Text](subl.md) -* [Atom](atom.md) - -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](contributing.md). - -## Overview - -* [Installation](#installation) -* [Configuration](#configuration) - -Learn more at the following pages: - -* [Features](features.md) -* [Command-line](command-line.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 - -```sh -go get golang.org/x/tools/gopls@latest -``` - -**Do not** use the `-u` flag, as it will update your dependencies to incompatible versions. - -To get a specific version of `gopls` (for example, to test a prerelease -version), run: - -```sh -go get golang.org/x/tools/gopls@vX.Y.Z -``` - -Where `vX.Y.Z` is the desired version. - -If you see this error: - -```sh -$ go get golang.org/x/tools/gopls@latest -go: cannot use path@version syntax in GOPATH mode -``` - -then run - -```sh -GO111MODULE=on go get golang.org/x/tools/gopls@latest -``` - -### Unstable versions - -`go get` doesn't honor the `replace` directive in the `go.mod` of -`gopls` when you are outside of the `gopls` module, so a simple `go get` -with `@master` could fail. To actually update your `gopls` to the -latest **unstable** version, use: - -```sh -go get golang.org/x/tools/gopls@master golang.org/x/tools@master -``` - -In general, you should use `@latest` instead, to prevent frequent -breakages. - -### Supported Go versions - -`gopls` follows the -[Go Release Policy](https://golang.org/doc/devel/release.html#policy), -meaning that it officially supports the last 2 major Go releases. We run CI to -verify that the `gopls` tests pass for the last 4 major Go releases, but do not -prioritize issues only affecting legacy Go release (3 or 4 releases ago). - -## Configuration - -### Environment variables - -These are often inherited from the editor that launches `gopls`, and sometimes -the editor has a way to add or replace values before launching. For example, -VSCode allows you to configure `go.toolsEnvVars`. - -Configuring your environment correctly is important, as `gopls` relies on the -`go` command. - -### Command-line flags - -See the [command-line page](command-line.md) for more information about the -flags you might specify. All editors support some way of adding flags to -`gopls`, for the most part you should not need to do this unless you have very -unusual requirements or are trying to [troubleshoot](troubleshooting.md#steps) -`gopls` behavior. - -### Editor settings - -For the most part these will be settings that control how the editor interacts -with or uses the results of `gopls`, not modifications to `gopls` itself. This -means they are not standardized across editors, and you will have to look at -the specific instructions for your editor integration to change them. - -#### The set of workspace folders - -This is one of the most important pieces of configuration. It is the set of -folders that gopls considers to be "roots" that it should consider files to -be a part of. - -If you are using modules there should be one of these per go.mod that you -are working on. If you do not open the right folders, very little will work. -**This is the most common misconfiguration of `gopls` that we see**. - -#### Global configuration - -There should be a way of declaring global settings for `gopls` inside the -editor. The settings block will be called `"gopls"` and contains a collection -of controls for `gopls` that the editor is not expected to understand or -control. - -In VSCode, this would be a section in your settings file that might look like -this: - -```json5 - "gopls": { - "usePlaceholders": true, - "completeUnimported": true - }, -``` - -See [Settings](settings.md) for more information about the available -configurations. - -#### Workspace folder configuration - -This contains exactly the same set of values that are in the global -configuration, but it is fetched for every workspace folder separately. -The editor can choose to respond with different values per-folder. - -### Working on the Go source distribution - -If you are working on the [Go project](https://go.googlesource.com/go) itself, -your `go` command will have to correspond to the version of the source you are -working on. That is, if you have downloaded the code to `$HOME/go`, your `go` -command should be the `$HOME/go/bin/go` executable that you built with -`make.bash` or equivalent. - -You can achieve this by adding the right version of `go` to your `PATH` (`export PATH=$HOME/go/bin:$PATH` on Unix systems) or by configuring your editor. In VS Code, you can use the `go.alternateTools` setting to point to the correct version of `go`: - -```json5 -{ - - "go.alternateTools": { - "go": "$HOME/bin/go" - } -} -``` diff --git a/gopls/doc/vscode.md b/gopls/doc/vscode.md deleted file mode 100644 index 11f8efa8316..00000000000 --- a/gopls/doc/vscode.md +++ /dev/null @@ -1,66 +0,0 @@ -# VS Code - -Use the [VS Code Go] plugin, with the following configuration: - -```json5 -"go.useLanguageServer": true, -``` - -As of February 2020, `gopls` will be enabled by default in [VS Code Go]. -To learn more, follow along with -[golang.vscode-go#1037](https://github.com/golang/vscode-go/issues/1037). - -```json5 -"gopls": { - // Add parameter placeholders when completing a function. - "ui.completion.usePlaceholders": true, - - // If true, enable additional analyses with staticcheck. - // Warning: This will significantly increase memory usage. - "ui.diagnostic.staticcheck": false, - - // For more customization, see - // see https://github.com/golang/vscode-go/blob/master/docs/settings.md. -} -``` - -To enable more detailed debug information, add the following to your VSCode settings: - -```json5 -"go.languageServerFlags": [ - "-rpc.trace", // for more detailed debug logging - "serve", - "--debug=localhost:6060", // Optional: investigate memory usage, see profiles -], -``` - -See the section on [command line](command-line.md) arguments for more -information about what these do, along with other things like -`--logfile=auto` that you might want to use. - -## Build tags and flags - -Build tags and flags will be automatically picked up from `"go.buildTags"` and -`"go.buildFlags"` settings. In the rare case that you don't want that default -behavior, you can still override the settings from the `gopls` section, using -`"gopls": { "build.buildFlags": [] }`. - -## Remote Development with `gopls` - -You can also make use of `gopls` with the -[VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) -extensions to enable full-featured Go development on a lightweight client -machine, while connected to a more powerful server machine. - -First, install the Remote Development extension of your choice, such as the -[Remote - SSH](https://code.visualstudio.com/docs/remote/ssh) extension. Once -you open a remote session in a new window, open the Extensions pane -(Ctrl+Shift+X) and you will see several different sections listed. In the -"Local - Installed" section, navigate to the Go extension and click -"Install in SSH: hostname". - -Once you have reloaded VS Code, you will be prompted to install `gopls` and other -Go-related tools. After one more reload, you should be ready to develop remotely -with VS Code and the Go extension. - -[VS Code Go]: https://github.com/golang/vscode-go diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md index 6f56bf5007a..ed30dae2915 100644 --- a/gopls/doc/workspace.md +++ b/gopls/doc/workspace.md @@ -19,16 +19,16 @@ contain that single module. Otherwise, you are working with multiple modules. ### Multiple modules -As of Jan 2020, if you are working with multiple modules, you will need to -create a "workspace folder" for each module. This means that each module has -its own scope, and features will not work across modules. We are currently -working on addressing this limitation--see details about +As of Jan 2021, if you are working with multiple modules or nested modules, you +will need to create a "workspace folder" for each module. This means that each +module has its own scope, and features will not work across modules. We are +currently working on addressing this limitation--see details about [experimental workspace module mode](#experimental-workspace-module-mode) below. In VS Code, you can create a workspace folder by setting up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces). -View the [documentation for your editor plugin](user.md#editor) to learn how to +View the [documentation for your editor plugin](../README.md#editor) to learn how to configure a workspace folder in your editor. #### Workspace module (experimental) diff --git a/gopls/go.mod b/gopls/go.mod index 8bfb4d38f21..a8f4e806a8d 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -14,3 +14,5 @@ require ( mvdan.cc/gofumpt v0.1.0 mvdan.cc/xurls/v2 v2.2.0 ) + +replace golang.org/x/tools => ../ diff --git a/gopls/internal/hooks/gen-licenses.sh b/gopls/internal/hooks/gen-licenses.sh index f0756ffe346..7d6bab79f54 100755 --- a/gopls/internal/hooks/gen-licenses.sh +++ b/gopls/internal/hooks/gen-licenses.sh @@ -6,13 +6,10 @@ set -o pipefail +output=$1 tempfile=$(mktemp) cd $(dirname $0) -modhash=$(sha256sum ../../go.sum | awk '{print $1}') -# Make sure we have the code for all the modules we depend on. -go mod download - cat > $tempfile <> $tempfile done -cat >> $tempfile << END -\` - -const licensesGeneratedFrom = "$modhash" -END -mv $tempfile licenses.go \ No newline at end of file +echo "\`" >> $tempfile +mv $tempfile $output \ No newline at end of file diff --git a/gopls/internal/hooks/licenses.go b/gopls/internal/hooks/licenses.go index ac5bdbbbe84..a1594654730 100644 --- a/gopls/internal/hooks/licenses.go +++ b/gopls/internal/hooks/licenses.go @@ -167,5 +167,3 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ` - -const licensesGeneratedFrom = "2f7edcd0817bc065239a9c855c8ca7e49dd04cc18b33db2a3149472e4ffaa829" diff --git a/gopls/internal/hooks/licenses_test.go b/gopls/internal/hooks/licenses_test.go index 28b114989d5..d2c4e34d0a3 100644 --- a/gopls/internal/hooks/licenses_test.go +++ b/gopls/internal/hooks/licenses_test.go @@ -5,19 +5,36 @@ package hooks import ( - "crypto/sha256" - "encoding/hex" + "bytes" "io/ioutil" + "os/exec" + "runtime" "testing" ) func TestLicenses(t *testing.T) { - sumBytes, err := ioutil.ReadFile("../../go.sum") + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + t.Skip("generating licenses only works on Unixes") + } + tmp, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + tmp.Close() + + if out, err := exec.Command("./gen-licenses.sh", tmp.Name()).CombinedOutput(); err != nil { + t.Fatalf("generating licenses failed: %q, %v", out, err) + } + + got, err := ioutil.ReadFile(tmp.Name()) + if err != nil { + t.Fatal(err) + } + want, err := ioutil.ReadFile("licenses.go") if err != nil { t.Fatal(err) } - sumSum := sha256.Sum256(sumBytes) - if licensesGeneratedFrom != hex.EncodeToString(sumSum[:]) { + if !bytes.Equal(got, want) { t.Error("combined license text needs updating. Run: `go generate ./internal/hooks` from the gopls module.") } } diff --git a/gopls/internal/regtest/bench_test.go b/gopls/internal/regtest/bench/bench_test.go similarity index 92% rename from gopls/internal/regtest/bench_test.go rename to gopls/internal/regtest/bench/bench_test.go index 5ad93589c87..1702e841feb 100644 --- a/gopls/internal/regtest/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -2,16 +2,22 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package bench import ( "flag" "fmt" "testing" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/protocol" ) +func TestMain(m *testing.M) { + Main(m) +} + func printBenchmarkResults(result testing.BenchmarkResult) { fmt.Println("Benchmark Statistics:") fmt.Println(result.String()) @@ -37,7 +43,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) {}) + WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {}) } }) @@ -72,7 +78,7 @@ func TestBenchmarkSymbols(t *testing.T) { } opts = append(opts, conf) - withOptions(opts...).run(t, "", func(t *testing.T, env *Env) { + 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. diff --git a/gopls/internal/regtest/completion_bench_test.go b/gopls/internal/regtest/bench/completion_bench_test.go similarity index 98% rename from gopls/internal/regtest/completion_bench_test.go rename to gopls/internal/regtest/bench/completion_bench_test.go index 267eeb6b326..be36d45ac09 100644 --- a/gopls/internal/regtest/completion_bench_test.go +++ b/gopls/internal/regtest/bench/completion_bench_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 regtest +package bench import ( "flag" @@ -11,6 +11,8 @@ import ( "strings" "testing" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/fake" ) @@ -45,7 +47,7 @@ func benchmarkCompletion(options completionBenchOptions, t *testing.T) { // it first (and therefore need hooks). opts = append(opts, SkipHooks(false)) - withOptions(opts...).run(t, "", func(t *testing.T, env *Env) { + WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { env.OpenFile(options.file) // Run edits required for this completion. diff --git a/gopls/internal/regtest/stress_test.go b/gopls/internal/regtest/bench/stress_test.go similarity index 94% rename from gopls/internal/regtest/stress_test.go rename to gopls/internal/regtest/bench/stress_test.go index 77f4ff23db7..8cdbcfe5399 100644 --- a/gopls/internal/regtest/stress_test.go +++ b/gopls/internal/regtest/bench/stress_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 regtest +package bench import ( "context" @@ -10,6 +10,8 @@ import ( "fmt" "testing" "time" + + . "golang.org/x/tools/gopls/internal/regtest" ) // Pilosa is a repository that has historically caused significant memory @@ -50,7 +52,7 @@ func TestPilosaStress(t *testing.T) { } opts := stressTestOptions(*pilosaPath) - withOptions(opts...).run(t, "", func(t *testing.T, env *Env) { + WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { files := []string{ "cmd.go", "internal/private.pb.go", diff --git a/gopls/internal/regtest/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go similarity index 83% rename from gopls/internal/regtest/codelens_test.go rename to gopls/internal/regtest/codelens/codelens_test.go index a58de79433b..52a7e113c11 100644 --- a/gopls/internal/regtest/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -2,14 +2,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package codelens import ( "runtime" "strings" "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" @@ -17,6 +18,10 @@ import ( "golang.org/x/tools/internal/testenv" ) +func TestMain(m *testing.M) { + Main(m) +} + func TestDisablingCodeLens(t *testing.T) { const workspace = ` -- go.mod -- @@ -53,11 +58,11 @@ const ( } for _, test := range tests { t.Run(test.label, func(t *testing.T) { - withOptions( + WithOptions( EditorConfig{ CodeLenses: test.enabled, }, - ).run(t, workspace, func(t *testing.T, env *Env) { + ).Run(t, workspace, func(t *testing.T, env *Env) { env.OpenFile("lib.go") lens := env.CodeLens("lib.go") if gotCodeLens := len(lens) > 0; gotCodeLens != test.wantCodeLens { @@ -111,14 +116,22 @@ func main() { _ = hi.Goodbye } ` + + const wantGoMod = `module mod.com + +go 1.12 + +require golang.org/x/hello v1.3.3 +` + for _, commandTitle := range []string{ "Upgrade transitive dependencies", "Upgrade direct dependencies", } { t.Run(commandTitle, func(t *testing.T) { - withOptions( + WithOptions( ProxyFiles(proxyWithLatest), - ).run(t, shouldUpdateDep, func(t *testing.T, env *Env) { + ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { env.OpenFile("go.mod") var lens protocol.CodeLens var found bool @@ -137,20 +150,27 @@ func main() { }); err != nil { t.Fatal(err) } - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1)) - got := env.Editor.BufferText("go.mod") - const wantGoMod = `module mod.com - -go 1.12 - -require golang.org/x/hello v1.3.3 -` - if got != wantGoMod { + 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)) } }) }) } + t.Run("Upgrade individual dependency", func(t *testing.T) { + WithOptions(ProxyFiles(proxyWithLatest)).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + env.ExecuteCodeLensCommand("go.mod", source.CommandCheckUpgrades) + d := &protocol.PublishDiagnosticsParams{} + env.Await(OnceMet(env.DiagnosticAtRegexpWithMessage("go.mod", `require`, "can be upgraded"), + ReadDiagnostics("go.mod", d))) + env.ApplyQuickFixes("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)) + } + }) + }) } func TestUnusedDependenciesCodelens(t *testing.T) { @@ -196,10 +216,10 @@ func main() { _ = hi.Goodbye } ` - runner.Run(t, shouldRemoveDep, func(t *testing.T, env *Env) { + WithOptions(ProxyFiles(proxy)).Run(t, shouldRemoveDep, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.ExecuteCodeLensCommand("go.mod", source.CommandTidy) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1)) + env.Await(env.DoneWithChangeWatchedFiles()) got := env.Editor.BufferText("go.mod") const wantGoMod = `module mod.com @@ -210,7 +230,7 @@ require golang.org/x/hello v1.0.0 if got != wantGoMod { t.Fatalf("go.mod tidy failed:\n%s", tests.Diff(t, wantGoMod, got)) } - }, ProxyFiles(proxy)) + }) } func TestRegenerateCgo(t *testing.T) { @@ -234,7 +254,7 @@ func Foo() { print(C.fortytwo()) } ` - runner.Run(t, workspace, func(t *testing.T, env *Env) { + Run(t, workspace, func(t *testing.T, env *Env) { // Open the file. We should have a nonexistant symbol. env.OpenFile("cgo.go") env.Await(env.DiagnosticAtRegexp("cgo.go", `C\.(fortytwo)`)) // could not determine kind of name for C.fortytwo @@ -271,12 +291,12 @@ func main() { fmt.Println(x) } ` - withOptions( + WithOptions( EditorConfig{ CodeLenses: map[string]bool{ "gc_details": true, }}, - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.ExecuteCodeLensCommand("main.go", source.CommandToggleDetails) d := &protocol.PublishDiagnosticsParams{} diff --git a/gopls/internal/regtest/completion_test.go b/gopls/internal/regtest/completion/completion_test.go similarity index 91% rename from gopls/internal/regtest/completion_test.go rename to gopls/internal/regtest/completion/completion_test.go index 412fdab6fe2..7b8f9669999 100644 --- a/gopls/internal/regtest/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -2,19 +2,43 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package completion import ( "fmt" "strings" "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) +func TestMain(m *testing.M) { + Main(m) +} + +const proxy = ` +-- example.com@v1.2.3/go.mod -- +module example.com + +go 1.12 +-- example.com@v1.2.3/blah/blah.go -- +package blah + +const Name = "Blah" +-- random.org@v1.2.3/go.mod -- +module random.org + +go 1.12 +-- random.org@v1.2.3/blah/blah.go -- +package hello + +const Name = "Hello" +` + func TestPackageCompletion(t *testing.T) { testenv.NeedsGo1Point(t, 14) const files = ` @@ -124,11 +148,11 @@ pac }, } { t.Run(tc.name, func(t *testing.T) { - run(t, files, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { if tc.content != nil { env.WriteWorkspaceFile(tc.filename, *tc.content) env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), ) } env.OpenFile(tc.filename) @@ -181,7 +205,7 @@ package ma ` want := []string{"ma", "ma_test", "main", "math", "math_test"} - run(t, files, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("math/add.go") completions := env.Completion("math/add.go", fake.Pos{ Line: 0, @@ -242,31 +266,29 @@ func _() { _ = blah.Hello } ` - withOptions( + WithOptions( ProxyFiles(proxy), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { // Make sure the dependency is in the module cache and accessible for // unimported completions, and then remove it before proceeding. env.RemoveWorkspaceFile("main2.go") env.RunGoCommand("mod", "tidy") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2)) + env.Await(env.DoneWithChangeWatchedFiles()) // Trigger unimported completions for the example.com/blah package. env.OpenFile("main.go") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) pos := env.RegexpSearch("main.go", "ah") completions := env.Completion("main.go", pos) if len(completions.Items) == 0 { t.Fatalf("no completion items") } env.AcceptCompletion("main.go", pos, completions.Items[0]) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1)) + env.Await(env.DoneWithChange()) // Trigger completions once again for the blah.<> selector. env.RegexpReplace("main.go", "_ = blah", "_ = blah.") - env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2), - ) + env.Await(env.DoneWithChange()) pos = env.RegexpSearch("main.go", "\n}") completions = env.Completion("main.go", pos) if len(completions.Items) != 1 { @@ -313,7 +335,7 @@ package mainmod const Name = "mainmod" ` - withOptions(ProxyFiles(proxy)).run(t, files, func(t *testing.T, env *Env) { + WithOptions(ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) { env.CreateBuffer("import.go", "package pkg\nvar _ = mainmod.Name\n") env.SaveBuffer("import.go") content := env.ReadWorkspaceFile("import.go") diff --git a/gopls/internal/regtest/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go similarity index 85% rename from gopls/internal/regtest/diagnostics_test.go rename to gopls/internal/regtest/diagnostics/diagnostics_test.go index 8aa45cafc89..50ea0b2d278 100644 --- a/gopls/internal/regtest/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_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 regtest +package diagnostics import ( "context" @@ -11,6 +11,8 @@ import ( "os" "testing" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp" "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" @@ -18,6 +20,10 @@ import ( "golang.org/x/tools/internal/testenv" ) +func TestMain(m *testing.M) { + Main(m) +} + // Use mod.com for all go.mod files due to golang/go#35230. const exampleProgram = ` -- go.mod -- @@ -37,7 +43,7 @@ func TestDiagnosticErrorInEditedFile(t *testing.T) { // This test is very basic: start with a clean Go program, make an error, and // get a diagnostic for that error. However, it also demonstrates how to // combine Expectations to await more complex state in the editor. - runner.Run(t, exampleProgram, func(t *testing.T, env *Env) { + Run(t, exampleProgram, func(t *testing.T, env *Env) { // Deleting the 'n' at the end of Println should generate a single error // diagnostic. env.OpenFile("main.go") @@ -46,7 +52,7 @@ func TestDiagnosticErrorInEditedFile(t *testing.T) { // Once we have gotten diagnostics for the change above, we should // satisfy the DiagnosticAtRegexp assertion. OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), + env.DoneWithChange(), env.DiagnosticAtRegexp("main.go", "Printl"), ), // Assert that this test has sent no error logs to the client. This is not @@ -65,7 +71,7 @@ module mod.com go 1.12 ` - runner.Run(t, onlyMod, func(t *testing.T, env *Env) { + Run(t, onlyMod, func(t *testing.T, env *Env) { env.CreateBuffer("main.go", `package main func m() { @@ -87,7 +93,7 @@ func TestDiagnosticErrorInNewFile(t *testing.T) { const Foo = "abc ` - runner.Run(t, brokenFile, func(t *testing.T, env *Env) { + Run(t, brokenFile, func(t *testing.T, env *Env) { env.CreateBuffer("broken.go", brokenFile) env.Await(env.DiagnosticAtRegexp("broken.go", "\"abc")) }) @@ -110,7 +116,7 @@ const a = 2 ` func TestDiagnosticClearingOnEdit(t *testing.T) { - runner.Run(t, badPackage, func(t *testing.T, env *Env) { + 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")) @@ -124,7 +130,7 @@ func TestDiagnosticClearingOnEdit(t *testing.T) { } func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { - runner.Run(t, badPackage, func(t *testing.T, env *Env) { + 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.RemoveWorkspaceFile("b.go") @@ -134,7 +140,7 @@ func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { } func TestDiagnosticClearingOnClose(t *testing.T) { - runner.Run(t, badPackage, func(t *testing.T, env *Env) { + Run(t, badPackage, func(t *testing.T, env *Env) { env.CreateBuffer("c.go", `package consts const a = 3`) @@ -152,7 +158,7 @@ const a = 3`) // Tests golang/go#37978. func TestIssue37978(t *testing.T) { - runner.Run(t, exampleProgram, func(t *testing.T, env *Env) { + Run(t, exampleProgram, func(t *testing.T, env *Env) { // Create a new workspace-level directory and empty file. env.CreateBuffer("c/c.go", "") @@ -203,7 +209,7 @@ func TestA(t *testing.T) { // Tests golang/go#38878: deleting a test file should clear its errors, and // not break the workspace. func TestDeleteTestVariant(t *testing.T) { - runner.Run(t, test38878, func(t *testing.T, env *Env) { + 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")) @@ -220,12 +226,12 @@ func TestDeleteTestVariant(t *testing.T) { // should not clear its errors. func TestDeleteTestVariant_DiskOnly(t *testing.T) { log.SetFlags(log.Lshortfile) - runner.Run(t, test38878, func(t *testing.T, env *Env) { + Run(t, test38878, func(t *testing.T, env *Env) { 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( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), DiagnosticAt("a_test.go", 5, 3))) }) } @@ -251,7 +257,7 @@ func Hello() { ` t.Run("manual", func(t *testing.T) { - runner.Run(t, noMod, func(t *testing.T, env *Env) { + Run(t, noMod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), ) @@ -276,7 +282,7 @@ func Hello() { }) }) t.Run("initialized", func(t *testing.T) { - runner.Run(t, noMod, func(t *testing.T, env *Env) { + Run(t, noMod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), ) @@ -289,9 +295,9 @@ func Hello() { }) t.Run("without workspace module", func(t *testing.T) { - withOptions( + WithOptions( Modes(Singleton), - ).run(t, noMod, func(t *testing.T, env *Env) { + ).Run(t, noMod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), ) @@ -339,7 +345,7 @@ func TestHello(t *testing.T) { } ` - runner.Run(t, testPackage, func(t *testing.T, env *Env) { + Run(t, testPackage, func(t *testing.T, env *Env) { env.OpenFile("lib_test.go") env.Await( DiagnosticAt("lib_test.go", 10, 2), @@ -365,7 +371,7 @@ go 1.12 package foo func main() {} ` - runner.Run(t, packageChange, func(t *testing.T, env *Env) { + Run(t, packageChange, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.RegexpReplace("a.go", "foo", "foox") env.Await( @@ -375,7 +381,7 @@ func main() {} // test to actually exercise the bug, we must wait until that work has // completed. OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), + env.DoneWithChange(), NoDiagnostics("a.go"), ), ) @@ -418,17 +424,19 @@ const Answer = 42 ` func TestResolveDiagnosticWithDownload(t *testing.T) { - runner.Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { + WithOptions( + ProxyFiles(testPackageWithRequireProxy), + ).Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { 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(env.DiagnosticAtRegexp("print.go", "fmt.Printf")) - }, ProxyFiles(testPackageWithRequireProxy)) + }) } func TestMissingDependency(t *testing.T) { - runner.Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { + Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { env.OpenFile("print.go") env.Await(LogMatching(protocol.Error, "initial workspace load failed", 1)) }) @@ -444,7 +452,7 @@ func Hello() { var x int } ` - runner.Run(t, adHoc, func(t *testing.T, env *Env) { + Run(t, adHoc, func(t *testing.T, env *Env) { env.OpenFile("b/b.go") env.Await(env.DiagnosticAtRegexp("b/b.go", "x")) }) @@ -460,13 +468,13 @@ func _() { fmt.Println("Hello World") } ` - withOptions( + WithOptions( EditorConfig{ Env: map[string]string{ "GOPATH": "", "GO111MODULE": "off", }, - }).run(t, files, func(t *testing.T, env *Env) { + }).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.Await(env.DiagnosticAtRegexp("main.go", "fmt")) env.SaveBuffer("main.go") @@ -491,7 +499,7 @@ 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(editorConfig).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OrganizeImports("main.go") env.Await(EmptyDiagnostics("main.go")) @@ -516,7 +524,7 @@ func _() { } } ` - runner.Run(t, generated, func(t *testing.T, env *Env) { + Run(t, generated, func(t *testing.T, env *Env) { env.OpenFile("main.go") original := env.ReadWorkspaceFile("main.go") var d protocol.PublishDiagnosticsParams @@ -549,7 +557,7 @@ func f() { hello.Goodbye() } ` - runner.Run(t, noModule, func(t *testing.T, env *Env) { + Run(t, noModule, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.Await( OutstandingWork(lsp.WorkspaceLoadFailure, "outside of a module"), @@ -568,13 +576,13 @@ hi mom ` for _, go111module := range []string{"on", "off", ""} { t.Run(fmt.Sprintf("GO111MODULE_%v", go111module), func(t *testing.T) { - withOptions(EditorConfig{ + WithOptions(EditorConfig{ Env: map[string]string{"GO111MODULE": go111module}, - }).run(t, files, func(t *testing.T, env *Env) { + }).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("hello.txt") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), + env.DoneWithOpen(), NoShowMessage(), ), ) @@ -601,7 +609,7 @@ func main() { fmt.Println("") } ` - runner.Run(t, collision, func(t *testing.T, env *Env) { + WithOptions(InGOPATH()).Run(t, collision, func(t *testing.T, env *Env) { env.OpenFile("x/main.go") env.Await( env.DiagnosticAtRegexp("x/main.go", "fmt.Println"), @@ -618,12 +626,12 @@ func main() { badFile := fmt.Sprintf("%s/found packages main (main.go) and x (x.go) in %s/src/x", dir, env.Sandbox.GOPATH()) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), + env.DoneWithChange(), EmptyDiagnostics("x/main.go"), ), NoDiagnostics(badFile), ) - }, InGOPATH()) + }) } const ardanLabsProxy = ` @@ -654,9 +662,9 @@ func main() { _ = conf.ErrHelpWanted } ` - withOptions( + WithOptions( ProxyFiles(ardanLabsProxy), - ).run(t, ardanLabs, func(t *testing.T, env *Env) { + ).Run(t, ardanLabs, func(t *testing.T, env *Env) { // Expect a diagnostic with a suggested fix to add // "github.com/ardanlabs/conf" to the go.mod file. env.OpenFile("go.mod") @@ -712,9 +720,9 @@ module mod.com go 1.12 -- main.go -- ` - withOptions( + WithOptions( ProxyFiles(ardanLabsProxy), - ).run(t, emptyFile, func(t *testing.T, env *Env) { + ).Run(t, emptyFile, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main @@ -757,22 +765,19 @@ func _() { fmt.Println("hi") } ` - runner.Run(t, simplePackage, func(t *testing.T, env *Env) { + Run(t, simplePackage, func(t *testing.T, env *Env) { env.OpenFile("a/a1.go") env.CreateBuffer("a/a2.go", ``) env.SaveBufferWithoutActions("a/a2.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1), + env.DoneWithSave(), NoDiagnostics("a/a1.go"), ), ) env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`)) env.Await( - OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), - NoDiagnostics("a/a1.go"), - ), + OnceMet(env.DoneWithChange(), NoDiagnostics("a/a1.go")), ) }) } @@ -805,7 +810,7 @@ func TestHello(t *testing.T) { Hello() } ` - runner.Run(t, testVariant, func(t *testing.T, env *Env) { + Run(t, testVariant, func(t *testing.T, env *Env) { // 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") @@ -861,12 +866,10 @@ go 1.12 package foo -- foo/bar_test.go -- ` - run(t, mod, func(t *testing.T, env *Env) { + Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("foo/bar_test.go") env.EditBuffer("foo/bar_test.go", fake.NewEdit(0, 0, 0, 0, "package foo")) - env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), - ) + env.Await(env.DoneWithChange()) env.RegexpReplace("foo/bar_test.go", "package foo", `package foo_test import "testing" @@ -894,17 +897,17 @@ package foo -- foo/bar_test.go -- package foo_ ` - run(t, mod, func(t *testing.T, env *Env) { + 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( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1), + env.DoneWithSave(), NoDiagnostics("foo/bar_test.go"), ), OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1), + env.DoneWithSave(), NoDiagnostics("foo/foo.go"), ), ) @@ -919,11 +922,11 @@ package x -- x_test.go -- ` - withOptions(InGOPATH()).run(t, files, func(t *testing.T, env *Env) { + WithOptions(InGOPATH()).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("x_test.go") env.EditBuffer("x_test.go", fake.NewEdit(0, 0, 0, 0, "pack")) env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), + env.DoneWithChange(), NoShowMessage(), ) }) @@ -940,11 +943,11 @@ package x var _ = foo.Bar ` - runner.Run(t, ws, func(t *testing.T, env *Env) { + Run(t, ws, func(t *testing.T, env *Env) { env.OpenFile("_foo/x.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), + env.DoneWithOpen(), NoDiagnostics("_foo/x.go"), )) }) @@ -978,9 +981,9 @@ import "mod.com/a" const C = a.A ` - runner.Run(t, ws, func(t *testing.T, env *Env) { + Run(t, ws, func(t *testing.T, env *Env) { env.OpenFile("b/b.go") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) // Delete c/c.go, the only file in package c. env.RemoveWorkspaceFile("c/c.go") @@ -1007,16 +1010,16 @@ go 1.12 // package loads. writeGoVim := func(env *Env, name, content string) { env.WriteWorkspaceFile(name, "") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1)) + env.Await(env.DoneWithChangeWatchedFiles()) env.CreateBuffer(name, "\n") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.EditBuffer(name, fake.NewEdit(1, 0, 1, 0, content)) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1)) + env.Await(env.DoneWithChange()) env.EditBuffer(name, fake.NewEdit(0, 0, 1, 0, "")) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1)) + env.Await(env.DoneWithChange()) } const p = `package p; func DoIt(s string) {};` @@ -1031,7 +1034,7 @@ func main() { // A simple version of the test that reproduces most of the problems it // exposes. t.Run("short", func(t *testing.T) { - runner.Run(t, mod, func(t *testing.T, env *Env) { + 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")) @@ -1040,7 +1043,7 @@ func main() { // A full version that replicates the whole flow of the test. t.Run("full", func(t *testing.T) { - runner.Run(t, mod, func(t *testing.T, env *Env) { + Run(t, mod, func(t *testing.T, env *Env) { writeGoVim(env, "p/p.go", p) writeGoVim(env, "main.go", main) writeGoVim(env, "p/p_test.go", `package p @@ -1091,10 +1094,10 @@ func _() { var x int } ` - withOptions( + WithOptions( // Empty workspace folders. WorkspaceFolders(), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( env.DiagnosticAtRegexp("a/a.go", "x"), @@ -1119,7 +1122,7 @@ func Foo() { fmt.Println("") } ` - runner.Run(t, basic, func(t *testing.T, env *Env) { + Run(t, basic, func(t *testing.T, env *Env) { testenv.NeedsGo1Point(t, 15) env.WriteWorkspaceFile("foo/foo_test.go", `package main @@ -1131,7 +1134,7 @@ func main() { env.RegexpReplace("foo/foo_test.go", `package main`, `package foo`) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), + env.DoneWithChange(), NoDiagnostics("foo/foo.go"), ), ) @@ -1149,15 +1152,15 @@ package main func main() {} ` - runner.Run(t, basic, func(t *testing.T, env *Env) { + Run(t, basic, func(t *testing.T, env *Env) { env.Editor.CreateBuffer(env.Ctx, "foo.go", `package main`) env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), + env.DoneWithOpen(), ) env.CloseBuffer("foo.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), 1), + env.DoneWithClose(), NoLogMatching(protocol.Info, "packages=0"), ), ) @@ -1174,31 +1177,25 @@ go 1.14 -- main2.go -- package main ` - runner.Run(t, basic, func(t *testing.T, env *Env) { + Run(t, basic, func(t *testing.T, env *Env) { env.CreateBuffer("main.go", "") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.SaveBufferWithoutActions("main.go") - env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1), - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), - ) + env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles()) env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main func main() { } `)) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1)) + env.Await(env.DoneWithChange()) env.SaveBuffer("main.go") - env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 2), - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2), - ) + env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles()) env.EditBuffer("main.go", fake.NewEdit(0, 0, 4, 0, "")) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2)) + env.Await(env.DoneWithChange()) env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main @@ -1226,7 +1223,7 @@ package default func main() {} ` - runner.Run(t, pkgDefault, func(t *testing.T, env *Env) { + Run(t, pkgDefault, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.Await( env.DiagnosticAtRegexp("main.go", "default"), @@ -1252,18 +1249,18 @@ func main() { var x int } ` - withOptions( + WithOptions( WorkspaceFolders("a"), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.Await( env.DiagnosticAtRegexp("main.go", "x"), ) }) - withOptions( + WithOptions( WorkspaceFolders("a"), LimitWorkspaceScope(), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.Await( NoDiagnostics("main.go"), @@ -1292,9 +1289,9 @@ func main() { } ` - withOptions( + WithOptions( EditorConfig{EnableStaticcheck: true}, - ).run(t, files, func(t *testing.T, env *Env) { + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") // Staticcheck should generate a diagnostic to simplify this literal. env.Await(env.DiagnosticAtRegexp("main.go", `t{"msg"}`)) @@ -1317,7 +1314,7 @@ func main() { package main func main() {} ` - runner.Run(t, dir, func(t *testing.T, env *Env) { + Run(t, dir, func(t *testing.T, env *Env) { log.SetFlags(log.Lshortfile) env.OpenFile("main.go") env.OpenFile("other.go") @@ -1368,7 +1365,7 @@ func _() { var x int } ` - run(t, files, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( env.DiagnosticAtRegexp("a/a.go", "x"), @@ -1395,11 +1392,11 @@ func b(c bytes.Buffer) { _ = 1 } ` - withOptions( + WithOptions( EditorConfig{ AllExperiments: true, }, - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { // Confirm that the setting doesn't cause any warnings. env.Await(NoShowMessage()) }) @@ -1442,7 +1439,7 @@ func main() { var x int } ` - run(t, mod, func(t *testing.T, env *Env) { + Run(t, mod, func(t *testing.T, env *Env) { env.Await( OnceMet( InitialWorkspaceLoad, @@ -1459,6 +1456,25 @@ func main() { func TestRenamePackage(t *testing.T) { testenv.NeedsGo1Point(t, 16) + const proxy = ` +-- example.com@v1.2.3/go.mod -- +module example.com + +go 1.12 +-- example.com@v1.2.3/blah/blah.go -- +package blah + +const Name = "Blah" +-- random.org@v1.2.3/go.mod -- +module random.org + +go 1.12 +-- random.org@v1.2.3/blah/blah.go -- +package hello + +const Name = "Hello" +` + const contents = ` -- go.mod -- module mod.com @@ -1480,17 +1496,17 @@ package foo package foo_ ` - withOptions( + WithOptions( ProxyFiles(proxy), InGOPATH(), - ).run(t, contents, func(t *testing.T, env *Env) { + ).Run(t, contents, func(t *testing.T, env *Env) { // Simulate typing character by character. env.OpenFile("foo/foo_test.go") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.RegexpReplace("foo/foo_test.go", "_", "_t") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1)) + env.Await(env.DoneWithChange()) env.RegexpReplace("foo/foo_test.go", "_t", "_test") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2)) + env.Await(env.DoneWithChange()) env.Await( EmptyDiagnostics("foo/foo_test.go"), @@ -1512,7 +1528,7 @@ go 1.12 -- main.go -- package main ` - run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.Await( OutstandingWork(lsp.WorkspaceLoadFailure, "unknown directive"), @@ -1556,7 +1572,7 @@ func main() { bob.Hello() } ` - run(t, mod, func(t *testing.T, env *Env) { + Run(t, mod, func(t *testing.T, env *Env) { env.RemoveWorkspaceFile("bob") env.Await( env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), @@ -1598,7 +1614,7 @@ package c import _ "mod.com/triple/a" ` - run(t, mod, func(t *testing.T, env *Env) { + 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"), @@ -1623,20 +1639,20 @@ import ( ) ` t.Run("module", func(t *testing.T) { - run(t, mod, func(t *testing.T, env *Env) { + 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"`), ) }) }) t.Run("GOPATH", func(t *testing.T) { - withOptions( + WithOptions( InGOPATH(), EditorConfig{ Env: map[string]string{"GO111MODULE": "off"}, }, Modes(Singleton), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexpWithMessage("main.go", `"nosuchpkg"`, `cannot find package "nosuchpkg" in any of`), ) @@ -1661,14 +1677,14 @@ package b ` for _, go111module := range []string{"on", "auto"} { t.Run("GO111MODULE="+go111module, func(t *testing.T) { - withOptions( + WithOptions( Modes(Singleton), EditorConfig{ Env: map[string]string{ "GO111MODULE": go111module, }, }, - ).run(t, modules, func(t *testing.T, env *Env) { + ).Run(t, modules, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("b/go.mod") env.Await( @@ -1682,7 +1698,7 @@ package b // Expect no warning if GO111MODULE=auto in a directory in GOPATH. t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { - withOptions( + WithOptions( Modes(Singleton), EditorConfig{ Env: map[string]string{ @@ -1690,11 +1706,11 @@ package b }, }, InGOPATH(), - ).run(t, modules, func(t *testing.T, env *Env) { + ).Run(t, modules, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), + env.DoneWithOpen(), NoDiagnostics("a/a.go"), ), NoOutstandingWork(), @@ -1747,13 +1763,13 @@ package hello func helloHelper() {} ` - withOptions( + WithOptions( ProxyFiles(proxy), Modes(Singleton), - ).run(t, nested, func(t *testing.T, env *Env) { + ).Run(t, nested, func(t *testing.T, env *Env) { // Expect a diagnostic in a nested module. env.OpenFile("nested/hello/hello.go") - didOpen := CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1) + didOpen := env.DoneWithOpen() env.Await( OnceMet( didOpen, @@ -1778,12 +1794,12 @@ package main func main() {} ` - run(t, nomod, func(t *testing.T, env *Env) { + 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( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), + env.DoneWithChange(), NoLogMatching(protocol.Info, "packages=1"), ), ) diff --git a/gopls/internal/regtest/env.go b/gopls/internal/regtest/env.go index 70859fd3b6e..322799d68c8 100644 --- a/gopls/internal/regtest/env.go +++ b/gopls/internal/regtest/env.go @@ -54,7 +54,7 @@ type State struct { // 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 - completedWork map[string]int + completedWork map[string]uint64 } type workProgress struct { @@ -119,7 +119,7 @@ func NewEnv(ctx context.Context, t *testing.T, sandbox *fake.Sandbox, ts servert state: State{ diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), outstandingWork: make(map[protocol.ProgressToken]*workProgress), - completedWork: make(map[string]int), + completedWork: make(map[string]uint64), }, waiters: make(map[int]*condition), } diff --git a/gopls/internal/regtest/env_test.go b/gopls/internal/regtest/env_test.go index 82fb17f81e6..e476be916e8 100644 --- a/gopls/internal/regtest/env_test.go +++ b/gopls/internal/regtest/env_test.go @@ -16,7 +16,7 @@ func TestProgressUpdating(t *testing.T) { e := &Env{ state: State{ outstandingWork: make(map[protocol.ProgressToken]*workProgress), - completedWork: make(map[string]int), + completedWork: make(map[string]uint64), }, } ctx := context.Background() diff --git a/gopls/internal/regtest/expectation.go b/gopls/internal/regtest/expectation.go index 6c479c35680..037faa45a08 100644 --- a/gopls/internal/regtest/expectation.go +++ b/gopls/internal/regtest/expectation.go @@ -199,6 +199,13 @@ func (e *Env) DoneWithChange() Expectation { return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes) } +// DoneWithSave expects all didSave notifications currently sent by the editor +// to be completely processed. +func (e *Env) DoneWithSave() Expectation { + saves := e.Editor.Stats().DidSave + return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves) +} + // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications // currently sent by the editor to be completely processed. func (e *Env) DoneWithChangeWatchedFiles() Expectation { @@ -206,11 +213,18 @@ func (e *Env) DoneWithChangeWatchedFiles() Expectation { return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes) } +// DoneWithClose expects all didClose notifications currently sent by the +// editor to be completely processed. +func (e *Env) DoneWithClose() Expectation { + changes := e.Editor.Stats().DidClose + return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes) +} + // CompletedWork expects a work item to have been completed >= atLeast times. // // 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, atLeast int) SimpleExpectation { +func CompletedWork(title string, atLeast uint64) SimpleExpectation { check := func(s State) Verdict { if s.completedWork[title] >= atLeast { return Met diff --git a/gopls/internal/regtest/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go similarity index 79% rename from gopls/internal/regtest/configuration_test.go rename to gopls/internal/regtest/misc/configuration_test.go index b61a8a85f52..d299e3f40a3 100644 --- a/gopls/internal/regtest/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/fake" ) @@ -25,16 +26,16 @@ package a // NotThisVariable should really start with ThisVariable. const ThisVariable = 7 ` - run(t, files, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), + env.DoneWithOpen(), NoDiagnostics("a/a.go"), ) cfg := &fake.EditorConfig{} *cfg = env.Editor.Config cfg.EnableStaticcheck = true - env.changeConfiguration(t, cfg) + env.ChangeConfiguration(t, cfg) env.Await( DiagnosticAt("a/a.go", 2, 0), ) diff --git a/gopls/internal/regtest/definition_test.go b/gopls/internal/regtest/misc/definition_test.go similarity index 91% rename from gopls/internal/regtest/definition_test.go rename to gopls/internal/regtest/misc/definition_test.go index 36a935238ef..a5e220c6e31 100644 --- a/gopls/internal/regtest/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -2,13 +2,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "path" "strings" "testing" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/tests" ) @@ -32,7 +34,7 @@ const message = "Hello World." ` func TestGoToInternalDefinition(t *testing.T) { - runner.Run(t, internalDefinition, func(t *testing.T, env *Env) { + Run(t, internalDefinition, func(t *testing.T, env *Env) { env.OpenFile("main.go") name, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "message")) if want := "const.go"; name != want { @@ -59,7 +61,7 @@ func main() { }` func TestGoToStdlibDefinition_Issue37045(t *testing.T) { - runner.Run(t, stdlibDefinition, func(t *testing.T, env *Env) { + 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)`)) if got, want := path.Base(name), "print.go"; got != want { @@ -79,7 +81,7 @@ func TestGoToStdlibDefinition_Issue37045(t *testing.T) { } func TestUnexportedStdlib_Issue40809(t *testing.T) { - runner.Run(t, stdlibDefinition, func(t *testing.T, env *Env) { + 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) @@ -120,7 +122,7 @@ func main() { var err error err.Error() }` - 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")) if content == nil { diff --git a/gopls/internal/regtest/failures_test.go b/gopls/internal/regtest/misc/failures_test.go similarity index 89% rename from gopls/internal/regtest/failures_test.go rename to gopls/internal/regtest/misc/failures_test.go index 4da6ce826cc..68bbded8c42 100644 --- a/gopls/internal/regtest/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "log" "testing" + + . "golang.org/x/tools/gopls/internal/regtest" ) // This test passes (TestHoverOnError in definition_test.go) without @@ -28,7 +30,7 @@ func main() { var err error err.Error() }` - withOptions(SkipLogs()).run(t, mod, func(t *testing.T, env *Env) { + WithOptions(SkipLogs()).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 @@ -56,7 +58,7 @@ const a = 2 ` func TestFailingDiagnosticClearingOnEdit(t *testing.T) { - runner.Run(t, badPackageDup, func(t *testing.T, env *Env) { + Run(t, badPackageDup, func(t *testing.T, env *Env) { log.SetFlags(log.Lshortfile) env.OpenFile("b.go") env.Await(env.AnyDiagnosticAtCurrentVersion("a.go")) diff --git a/gopls/internal/regtest/fix_test.go b/gopls/internal/regtest/misc/fix_test.go similarity index 92% rename from gopls/internal/regtest/fix_test.go rename to gopls/internal/regtest/misc/fix_test.go index e513148de12..a2566093316 100644 --- a/gopls/internal/regtest/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "testing" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/tests" ) @@ -27,7 +29,7 @@ func Foo() { _ = types.Info{} } ` - runner.Run(t, basic, func(t *testing.T, env *Env) { + Run(t, basic, func(t *testing.T, env *Env) { env.OpenFile("main.go") if err := env.Editor.RefactorRewrite(env.Ctx, "main.go", &protocol.Range{ Start: protocol.Position{ diff --git a/gopls/internal/regtest/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go similarity index 88% rename from gopls/internal/regtest/formatting_test.go rename to gopls/internal/regtest/misc/formatting_test.go index 63fa0ce10bc..0ad5fbb7870 100644 --- a/gopls/internal/regtest/formatting_test.go +++ b/gopls/internal/regtest/misc/formatting_test.go @@ -2,13 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "strings" "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/tests" ) @@ -30,7 +31,7 @@ func main() { ` func TestFormatting(t *testing.T) { - runner.Run(t, unformattedProgram, func(t *testing.T, env *Env) { + Run(t, unformattedProgram, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.FormatBuffer("main.go") got := env.Editor.BufferText("main.go") @@ -52,7 +53,7 @@ package main func f() {} ` - runner.Run(t, onelineProgram, func(t *testing.T, env *Env) { + Run(t, onelineProgram, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.FormatBuffer("a.go") got := env.Editor.BufferText("a.go") @@ -76,7 +77,7 @@ import "fmt" func f() { fmt.Println() } ` - runner.Run(t, onelineProgramA, func(t *testing.T, env *Env) { + Run(t, onelineProgramA, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.OrganizeImports("a.go") got := env.Editor.BufferText("a.go") @@ -97,7 +98,7 @@ package x func f() {} ` - runner.Run(t, onelineProgramB, func(t *testing.T, env *Env) { + Run(t, onelineProgramB, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.OrganizeImports("a.go") got := env.Editor.BufferText("a.go") @@ -143,7 +144,7 @@ func main() { ` func TestOrganizeImports(t *testing.T) { - runner.Run(t, disorganizedProgram, func(t *testing.T, env *Env) { + Run(t, disorganizedProgram, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OrganizeImports("main.go") got := env.Editor.BufferText("main.go") @@ -155,7 +156,7 @@ func TestOrganizeImports(t *testing.T) { } func TestFormattingOnSave(t *testing.T) { - runner.Run(t, disorganizedProgram, func(t *testing.T, env *Env) { + Run(t, disorganizedProgram, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.SaveBuffer("main.go") got := env.Editor.BufferText("main.go") @@ -225,10 +226,10 @@ type Tree struct { }, } { t.Run(tt.issue, func(t *testing.T) { - run(t, "-- main.go --", func(t *testing.T, env *Env) { + Run(t, "-- main.go --", func(t *testing.T, env *Env) { crlf := strings.ReplaceAll(tt.want, "\n", "\r\n") env.CreateBuffer("main.go", crlf) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.OrganizeImports("main.go") got := env.Editor.BufferText("main.go") got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity diff --git a/gopls/internal/regtest/generate_test.go b/gopls/internal/regtest/misc/generate_test.go similarity index 81% rename from gopls/internal/regtest/generate_test.go rename to gopls/internal/regtest/misc/generate_test.go index 87e64dfd8e5..69879242ba0 100644 --- a/gopls/internal/regtest/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -6,12 +6,12 @@ // +build !android -package regtest +package misc import ( "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" ) func TestGenerateProgress(t *testing.T) { @@ -40,14 +40,14 @@ func GetAnswer() int { //go:generate go run generate.go ` - runner.Run(t, generatedWorkspace, func(t *testing.T, env *Env) { + Run(t, generatedWorkspace, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("lib/lib.go", "answer"), ) env.RunGenerate("./lib") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), EmptyDiagnostics("lib/lib.go")), ) }) diff --git a/gopls/internal/regtest/imports_test.go b/gopls/internal/regtest/misc/imports_test.go similarity index 92% rename from gopls/internal/regtest/imports_test.go rename to gopls/internal/regtest/misc/imports_test.go index 1c52d59fc4a..9a95208cb5b 100644 --- a/gopls/internal/regtest/imports_test.go +++ b/gopls/internal/regtest/misc/imports_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 regtest +package misc import ( "io/ioutil" @@ -11,6 +11,8 @@ import ( "strings" "testing" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -42,7 +44,7 @@ func TestZ(t *testing.T) { // it was returning // "package main\nimport \"testing\"\npackage main..." - runner.Run(t, needs, func(t *testing.T, env *Env) { + 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") @@ -69,7 +71,7 @@ func main() { // The file remains unchanged, but if there are any CodeActions returned, they confuse vim. // Therefore check for no CodeActions - runner.Run(t, "", func(t *testing.T, env *Env) { + Run(t, "", func(t *testing.T, env *Env) { env.CreateBuffer("main.go", vim1) env.OrganizeImports("main.go") actions := env.CodeAction("main.go") @@ -102,7 +104,7 @@ func main() { } ` - runner.Run(t, "", func(t *testing.T, env *Env) { + Run(t, "", func(t *testing.T, env *Env) { env.CreateBuffer("main.go", vim2) env.OrganizeImports("main.go") actions := env.CodeAction("main.go") @@ -152,10 +154,10 @@ var _, _ = x.X, y.Y } defer os.RemoveAll(modcache) editorConfig := EditorConfig{Env: map[string]string{"GOMODCACHE": modcache}} - withOptions( + WithOptions( editorConfig, ProxyFiles(proxy), - ).run(t, files, func(t *testing.T, env *Env) { + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.Await(env.DiagnosticAtRegexp("main.go", `y.Y`)) env.SaveBuffer("main.go") @@ -197,7 +199,7 @@ func TestA(t *testing.T) { os.Stat("") } ` - run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") var d protocol.PublishDiagnosticsParams env.Await( diff --git a/gopls/internal/regtest/link_test.go b/gopls/internal/regtest/misc/link_test.go similarity index 94% rename from gopls/internal/regtest/link_test.go rename to gopls/internal/regtest/misc/link_test.go index 1e662a4ed4e..320a3eac421 100644 --- a/gopls/internal/regtest/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -2,12 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "strings" "testing" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/testenv" ) @@ -42,7 +44,9 @@ package pkg const Hello = "Hello" ` - runner.Run(t, program, func(t *testing.T, env *Env) { + WithOptions( + ProxyFiles(proxy), + ).Run(t, program, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OpenFile("go.mod") @@ -87,5 +91,5 @@ const Hello = "Hello" if len(links) != 0 { t.Errorf("documentLink: got %d document links for go.mod, want 0\nlinks: %v", len(links), links) } - }, ProxyFiles(proxy)) + }) } diff --git a/gopls/internal/regtest/misc/misc_test.go b/gopls/internal/regtest/misc/misc_test.go new file mode 100644 index 00000000000..0f424706071 --- /dev/null +++ b/gopls/internal/regtest/misc/misc_test.go @@ -0,0 +1,15 @@ +// 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 misc + +import ( + "testing" + + "golang.org/x/tools/gopls/internal/regtest" +) + +func TestMain(m *testing.M) { + regtest.Main(m) +} diff --git a/gopls/internal/regtest/references_test.go b/gopls/internal/regtest/misc/references_test.go similarity index 88% rename from gopls/internal/regtest/references_test.go rename to gopls/internal/regtest/misc/references_test.go index db94ed8c19a..93276362af8 100644 --- a/gopls/internal/regtest/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "testing" + + . "golang.org/x/tools/gopls/internal/regtest" ) func TestStdlibReferences(t *testing.T) { @@ -24,7 +26,7 @@ func main() { } ` - run(t, files, func(t *testing.T, env *Env) { + 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) diff --git a/gopls/internal/regtest/shared_test.go b/gopls/internal/regtest/misc/shared_test.go similarity index 78% rename from gopls/internal/regtest/shared_test.go rename to gopls/internal/regtest/misc/shared_test.go index 10c39b068af..376d378ae7d 100644 --- a/gopls/internal/regtest/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "testing" + + . "golang.org/x/tools/gopls/internal/regtest" ) const sharedProgram = ` @@ -22,19 +24,19 @@ func main() { fmt.Println("Hello World.") }` -func runShared(t *testing.T, program string, testFunc func(env1 *Env, env2 *Env)) { +func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) { // Only run these tests in forwarded modes. - modes := runner.DefaultModes & (Forwarded | SeparateProcess) - runner.Run(t, sharedProgram, func(t *testing.T, env1 *Env) { + modes := DefaultModes() & (Forwarded | SeparateProcess) + 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) testFunc(env1, env2) - }, Modes(modes)) + }) } func TestSimultaneousEdits(t *testing.T) { - runShared(t, exampleProgram, func(env1 *Env, env2 *Env) { + runShared(t, func(env1 *Env, env2 *Env) { // In editor #1, break fmt.Println as before. env1.OpenFile("main.go") env1.RegexpReplace("main.go", "Printl(n)", "") @@ -49,7 +51,7 @@ func TestSimultaneousEdits(t *testing.T) { } func TestShutdown(t *testing.T) { - runShared(t, sharedProgram, func(env1 *Env, env2 *Env) { + runShared(t, func(env1 *Env, env2 *Env) { env1.CloseEditor() // Now make an edit in editor #2 to trigger diagnostics. env2.OpenFile("main.go") diff --git a/gopls/internal/regtest/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go similarity index 91% rename from gopls/internal/regtest/vendor_test.go rename to gopls/internal/regtest/misc/vendor_test.go index f9d43ee9401..026309095e7 100644 --- a/gopls/internal/regtest/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package misc import ( "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/testenv" @@ -49,10 +50,10 @@ func _() { } ` // TODO(rstambler): Remove this when golang/go#41819 is resolved. - withOptions( + WithOptions( Modes(Singleton), ProxyFiles(basicProxy), - ).run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) { + ).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) { env.OpenFile("a/a1.go") env.Await( // The editor should pop up a message suggesting that the user @@ -61,7 +62,7 @@ func _() { // so once we see the request, we can assume that `go mod vendor` // will be executed. OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), + env.DoneWithOpen(), env.DiagnosticAtRegexp("go.mod", "module mod.com"), ), ) diff --git a/gopls/internal/regtest/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go similarity index 86% rename from gopls/internal/regtest/modfile_test.go rename to gopls/internal/regtest/modfile/modfile_test.go index faef053250a..86154db9d00 100644 --- a/gopls/internal/regtest/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -2,19 +2,47 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package regtest +package modfile import ( "path/filepath" "strings" "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/tests" "golang.org/x/tools/internal/testenv" ) +func TestMain(m *testing.M) { + Main(m) +} + +const workspaceProxy = ` +-- example.com@v1.2.3/go.mod -- +module example.com + +go 1.12 +-- example.com@v1.2.3/blah/blah.go -- +package blah + +func SaySomething() { + fmt.Println("something") +} +-- random.org@v1.2.3/go.mod -- +module random.org + +go 1.12 +-- random.org@v1.2.3/bye/bye.go -- +package bye + +func Goodbye() { + println("Bye") +} +` + const proxy = ` -- example.com@v1.2.3/go.mod -- module example.com @@ -51,13 +79,13 @@ func main() { } ` - runner := runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, + runner := RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, } t.Run("basic", func(t *testing.T) { - runner.run(t, untidyModule, func(t *testing.T, env *Env) { + runner.Run(t, untidyModule, func(t *testing.T, env *Env) { // Open the file and make sure that the initial workspace load does not // modify the go.mod file. goModContent := env.ReadWorkspaceFile("a/go.mod") @@ -84,7 +112,7 @@ func main() { 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) { + 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") @@ -92,9 +120,9 @@ func main() { env.RemoveWorkspaceFile("a/main.go") env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1), - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2), + env.DoneWithOpen(), + env.DoneWithSave(), + env.DoneWithChangeWatchedFiles(), ) env.WriteWorkspaceFile("main.go", mainContent) @@ -131,10 +159,10 @@ go 1.12 require example.com v1.2.3 ` - runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, - }.run(t, mod, func(t *testing.T, env *Env) { + RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, + }.Run(t, mod, func(t *testing.T, env *Env) { if strings.Contains(t.Name(), "workspace_module") { t.Skip("workspace module mode doesn't set -mod=readonly") } @@ -184,10 +212,10 @@ go 1.12 require random.org v1.2.3 ` - runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, - }.run(t, mod, func(t *testing.T, env *Env) { + RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, + }.Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams env.Await( @@ -237,10 +265,10 @@ go 1.12 require example.com v1.2.3 ` - runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, - }.run(t, mod, func(t *testing.T, env *Env) { + RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, + }.Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") var d protocol.PublishDiagnosticsParams env.Await( @@ -282,10 +310,10 @@ func main() {} go 1.14 ` - runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, - }.run(t, files, func(t *testing.T, env *Env) { + RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, + }.Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") var d protocol.PublishDiagnosticsParams env.Await( @@ -345,10 +373,10 @@ func _() { caire.RemoveTempImage() }` - runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, - }.run(t, repro, func(t *testing.T, env *Env) { + RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, + }.Run(t, repro, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams env.Await( @@ -395,10 +423,10 @@ package main func main() { fmt.Println(blah.Name) ` - runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, - }.run(t, mod, func(t *testing.T, env *Env) { + RunMultiple{ + {"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( @@ -452,10 +480,10 @@ import "example.com/blah/v2" var _ = blah.Name ` - runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, - }.run(t, files, func(t *testing.T, env *Env) { + RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, + }.Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.OpenFile("a/go.mod") var d protocol.PublishDiagnosticsParams @@ -503,13 +531,13 @@ func main() { } ` - runner := runMultiple{ - {"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(proxy))}, + runner := RunMultiple{ + {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(proxy))}, } // Start from a bad state/bad IWL, and confirm that we recover. t.Run("bad", func(t *testing.T) { - runner.run(t, unknown, func(t *testing.T, env *Env) { + 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"), @@ -555,7 +583,7 @@ func main() { // Start from a good state, transform to a bad state, and confirm that we // still recover. t.Run("good", func(t *testing.T) { - runner.run(t, known, func(t *testing.T, env *Env) { + runner.Run(t, known, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") env.Await( env.DiagnosticAtRegexp("a/main.go", "x = "), @@ -615,10 +643,10 @@ func main() { println(blah.Name) } ` - runMultiple{ - {"default", withOptions(ProxyFiles(badProxy), WorkspaceFolders("a"))}, - {"nested", withOptions(ProxyFiles(badProxy))}, - }.run(t, module, func(t *testing.T, env *Env) { + RunMultiple{ + {"default", WithOptions(ProxyFiles(badProxy), WorkspaceFolders("a"))}, + {"nested", WithOptions(ProxyFiles(badProxy))}, + }.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"), @@ -642,7 +670,7 @@ func main() { println(blah.Name) } ` - withOptions( + WithOptions( EditorConfig{ Env: map[string]string{ "GOFLAGS": "-mod=readonly", @@ -650,7 +678,7 @@ func main() { }, ProxyFiles(proxy), Modes(Singleton), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") original := env.ReadWorkspaceFile("go.mod") env.Await( @@ -673,19 +701,22 @@ func TestMultiModuleModDiagnostics(t *testing.T) { const mod = ` -- a/go.mod -- -module mod.com +module moda.com go 1.14 require ( example.com v1.2.3 ) +-- a/go.sum -- +example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= -- a/main.go -- package main func main() {} -- b/go.mod -- -module mod.com +module modb.com go 1.14 -- b/main.go -- @@ -697,13 +728,13 @@ func main() { blah.SaySomething() } ` - withOptions( + WithOptions( ProxyFiles(workspaceProxy), Modes(Experimental), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.3"), - env.DiagnosticAtRegexp("b/go.mod", "module mod.com"), + env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "is not used"), + env.DiagnosticAtRegexpWithMessage("b/go.mod", "module modb.com", "not in your go.mod file"), ) }) } @@ -727,12 +758,12 @@ func main() { blah.SaySomething() } ` - withOptions( + WithOptions( ProxyFiles(workspaceProxy), EditorConfig{ BuildFlags: []string{"-tags", "bob"}, }, - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"example.com/blah"`), ) @@ -750,7 +781,7 @@ package main func main() {} ` - run(t, mod, func(t *testing.T, env *Env) { + Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.RegexpReplace("go.mod", "module", "modul") env.Await( @@ -783,10 +814,10 @@ func main() { println(blah.Name) } ` - withOptions( + WithOptions( Modes(Singleton), // workspace modules don't use -mod=readonly (golang/go#43346) ProxyFiles(workspaceProxy), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { d := &protocol.PublishDiagnosticsParams{} env.OpenFile("go.mod") env.Await( @@ -821,14 +852,14 @@ package main func hello() {} ` - withOptions( + WithOptions( // 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), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.RegexpReplace("go.mod", "module", "modul") // Confirm that we still have metadata with only on-disk edits. env.OpenFile("main.go") @@ -892,9 +923,9 @@ package main func main() {} ` - withOptions( + WithOptions( ProxyFiles(proxy), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { d := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( @@ -934,9 +965,9 @@ package main func main() {} ` - withOptions( + WithOptions( ProxyFiles(proxy), - ).run(t, mod, func(t *testing.T, env *Env) { + ).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") @@ -992,10 +1023,10 @@ func main() { blah.Hello() } ` - withOptions( + WithOptions( ProxyFiles(workspaceProxy), Modes(Singleton), - ).run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") pos := env.RegexpSearch("go.mod", "example.com") params := &protocol.PublishDiagnosticsParams{} diff --git a/gopls/internal/regtest/reg_test.go b/gopls/internal/regtest/regtest.go similarity index 75% rename from gopls/internal/regtest/reg_test.go rename to gopls/internal/regtest/regtest.go index 25accb1d09b..87ff3f6201a 100644 --- a/gopls/internal/regtest/reg_test.go +++ b/gopls/internal/regtest/regtest.go @@ -30,14 +30,14 @@ var ( var runner *Runner type regtestRunner interface { - run(t *testing.T, files string, f TestFunc) + Run(t *testing.T, files string, f TestFunc) } -func run(t *testing.T, files string, f TestFunc) { +func Run(t *testing.T, files string, f TestFunc) { runner.Run(t, files, f) } -func withOptions(opts ...RunOption) configuredRunner { +func WithOptions(opts ...RunOption) configuredRunner { return configuredRunner{opts: opts} } @@ -45,23 +45,35 @@ type configuredRunner struct { opts []RunOption } -func (r configuredRunner) run(t *testing.T, files string, f TestFunc) { +func (r configuredRunner) Run(t *testing.T, files string, f TestFunc) { runner.Run(t, files, f, r.opts...) } -type runMultiple []struct { - name string - runner regtestRunner +type RunMultiple []struct { + Name string + Runner regtestRunner } -func (r runMultiple) run(t *testing.T, files string, f TestFunc) { +func (r RunMultiple) Run(t *testing.T, files string, f TestFunc) { for _, runner := range r { - t.Run(runner.name, func(t *testing.T) { - runner.runner.run(t, files, f) + t.Run(runner.Name, func(t *testing.T) { + runner.Runner.Run(t, files, f) }) } } -func TestMain(m *testing.M) { + +func DefaultModes() Mode { + if *runSubprocessTests { + return NormalModes | SeparateProcess + } + return NormalModes +} + +// Main sets up and tears down the shared regtest state. +// +// TODO(rFindley): This is probably not necessary, and complicates things now +// that we have multiple regtest suites. Consider removing. +func Main(m *testing.M) { flag.Parse() if os.Getenv("_GOPLS_TEST_BINARY_RUN_AS_GOPLS") == "true" { tool.Main(context.Background(), cmd.New("gopls", "", nil, nil), os.Args[1:]) @@ -69,7 +81,7 @@ func TestMain(m *testing.M) { } runner = &Runner{ - DefaultModes: NormalModes, + DefaultModes: DefaultModes(), Timeout: *regtestTimeout, PrintGoroutinesOnFailure: *printGoroutinesOnFailure, SkipCleanup: *skipCleanup, @@ -83,7 +95,6 @@ func TestMain(m *testing.M) { panic(fmt.Sprintf("finding test binary path: %v", err)) } } - runner.DefaultModes = NormalModes | SeparateProcess runner.GoplsPath = goplsPath } dir, err := ioutil.TempDir("", "gopls-regtest-") diff --git a/gopls/internal/regtest/runner.go b/gopls/internal/regtest/runner.go index 1e77cb8ef35..3348040caa8 100644 --- a/gopls/internal/regtest/runner.go +++ b/gopls/internal/regtest/runner.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "fmt" - exec "golang.org/x/sys/execabs" "io" "io/ioutil" "net" @@ -20,6 +19,8 @@ import ( "testing" "time" + exec "golang.org/x/sys/execabs" + "golang.org/x/tools/gopls/internal/hooks" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" @@ -308,7 +309,6 @@ var longBuilders = map[string]string{ "netbsd-arm-bsiegert": "", "solaris-amd64-oraclerel": "", "windows-arm-zx2c4": "", - "android-amd64-emu": "golang.org/issue/43554", } func checkBuilder(t *testing.T) { diff --git a/gopls/internal/regtest/watch_test.go b/gopls/internal/regtest/watch/watch_test.go similarity index 83% rename from gopls/internal/regtest/watch_test.go rename to gopls/internal/regtest/watch/watch_test.go index 3802765d36e..436c09153a0 100644 --- a/gopls/internal/regtest/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -7,12 +7,17 @@ package regtest import ( "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) +func TestMain(m *testing.M) { + Main(m) +} + func TestEditFile(t *testing.T) { const pkg = ` -- go.mod -- @@ -29,7 +34,7 @@ func _() { // Edit the file when it's *not open* in the workspace, and check that // diagnostics are updated. t.Run("unopened", func(t *testing.T) { - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("a/a.go", "x"), ) @@ -43,16 +48,16 @@ func _() { // Edit the file when it *is open* in the workspace, and check that // diagnostics are *not* updated. t.Run("opened", func(t *testing.T) { - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") // 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(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), env.DiagnosticAtRegexp("a/a.go", "x"), )) }) @@ -81,9 +86,9 @@ func _() { _ = b.B() } ` - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`) env.Await( env.DiagnosticAtRegexp("a/a.go", "b.B"), @@ -115,7 +120,7 @@ func _() { _ = b.B() } ` - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("a/a.go", "x"), ) @@ -158,9 +163,9 @@ func _() { _ = b.B() } ` - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) + env.Await(env.DoneWithOpen()) env.RemoveWorkspaceFile("b/b.go") env.Await( env.DiagnosticAtRegexp("a/a.go", "\"mod.com/b\""), @@ -190,7 +195,7 @@ func _() { c.C() } ` - runner.Run(t, missing, func(t *testing.T, env *Env) { + Run(t, missing, func(t *testing.T, env *Env) { t.Skip("the initial workspace load fails and never retries") env.Await( @@ -216,7 +221,7 @@ package a func _() {} ` - runner.Run(t, original, func(t *testing.T, env *Env) { + 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( @@ -239,7 +244,7 @@ func _() { hello() } ` - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("a/a.go", "hello"), ) @@ -314,11 +319,11 @@ func _() { // Add the new method before the implementation. Expect diagnostics. t.Run("method before implementation", func(t *testing.T) { - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.WriteWorkspaceFile("b/b.go", newMethod) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), DiagnosticAt("a/a.go", 12, 12), ), ) @@ -330,11 +335,11 @@ func _() { }) // Add the new implementation before the new method. Expect no diagnostics. t.Run("implementation before method", func(t *testing.T) { - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.WriteWorkspaceFile("a/a.go", implementation) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("a/a.go"), ), ) @@ -346,14 +351,14 @@ func _() { }) // Add both simultaneously. Expect no diagnostics. t.Run("implementation and method simultaneously", func(t *testing.T) { - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.WriteWorkspaceFiles(map[string]string{ "a/a.go": implementation, "b/b.go": newMethod, }) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("a/a.go"), ), NoDiagnostics("b/b.go"), @@ -380,14 +385,14 @@ func _() { package a ` t.Run("close then delete", func(t *testing.T) { - withOptions(EditorConfig{ + WithOptions(EditorConfig{ VerboseOutput: true, - }).run(t, pkg, func(t *testing.T, env *Env) { + }).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2), + env.DoneWithOpen(), LogMatching(protocol.Info, "a_unneeded.go", 1), ), ) @@ -402,7 +407,7 @@ package a env.SaveBuffer("a/a.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1), + 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. @@ -414,14 +419,14 @@ package a }) t.Run("delete then close", func(t *testing.T) { - withOptions( + WithOptions( EditorConfig{VerboseOutput: true}, - ).run(t, pkg, func(t *testing.T, env *Env) { + ).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2), + env.DoneWithOpen(), LogMatching(protocol.Info, "a_unneeded.go", 1), ), ) @@ -436,7 +441,7 @@ package a env.SaveBuffer("a/a.go") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1), + 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. @@ -479,7 +484,7 @@ package a func _() {} ` - runner.Run(t, pkg, func(t *testing.T, env *Env) { + Run(t, pkg, func(t *testing.T, env *Env) { env.ChangeFilesOnDisk([]fake.FileEvent{ { Path: "a/a3.go", @@ -510,7 +515,7 @@ var Hello int }) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("main.go"), ), ) @@ -565,7 +570,7 @@ func main() { blah.X() } ` - withOptions(ProxyFiles(proxy)).run(t, mod, func(t *testing.T, env *Env) { + WithOptions(ProxyFiles(proxy)).Run(t, mod, func(t *testing.T, env *Env) { env.WriteWorkspaceFiles(map[string]string{ "go.mod": `module mod.com @@ -585,7 +590,7 @@ func main() { `, }) env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("main.go"), ) }) @@ -609,10 +614,10 @@ func main() { _ = blah.Name } ` - withOptions( + WithOptions( InGOPATH(), Modes(Experimental), // module is in a subdirectory - ).run(t, files, func(t *testing.T, env *Env) { + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/main.go") env.Await(env.DiagnosticAtRegexp("foo/main.go", `"blah"`)) if err := env.Sandbox.RunGoCommand(env.Ctx, "foo", "mod", []string{"init", "mod.com"}); err != nil { @@ -620,7 +625,7 @@ func main() { } env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), env.DiagnosticAtRegexp("foo/main.go", `"blah"`), ), ) @@ -653,14 +658,14 @@ func main() { _ = blah.Name } ` - withOptions( + WithOptions( InGOPATH(), - ).run(t, files, func(t *testing.T, env *Env) { + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/main.go") env.RemoveWorkspaceFile("foo/go.mod") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), env.DiagnosticAtRegexp("foo/main.go", `"mod.com/blah"`), ), ) @@ -690,7 +695,7 @@ func TestBob(t *testing.T) { bob() } ` - run(t, files, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { // Add a new symbol to the package under test and use it in the test // variant. Expect no diagnostics. env.WriteWorkspaceFiles(map[string]string{ @@ -711,11 +716,11 @@ func TestAll(t *testing.T) { }) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("a/a.go"), ), OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("a/a_test.go"), ), ) @@ -743,11 +748,11 @@ func TestSomething(t *testing.T) {} }) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("a/a_test.go"), ), OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2), + env.DoneWithChangeWatchedFiles(), NoDiagnostics("a/a2_test.go"), ), ) diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go similarity index 87% rename from gopls/internal/regtest/workspace_test.go rename to gopls/internal/regtest/workspace/workspace_test.go index f4c21f7a6a5..cad358e692a 100644 --- a/gopls/internal/regtest/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_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 regtest +package workspace import ( "fmt" @@ -12,12 +12,17 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) +func TestMain(m *testing.M) { + Main(m) +} + const workspaceProxy = ` -- example.com@v1.2.3/go.mod -- module example.com @@ -117,7 +122,7 @@ func TestReferences(t *testing.T) { if tt.rootPath != "" { opts = append(opts, WorkspaceFolders(tt.rootPath)) } - withOptions(opts...).run(t, workspaceModule, func(t *testing.T, env *Env) { + 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`)) @@ -135,10 +140,10 @@ func TestReferences(t *testing.T) { // VS Code, where clicking on a reference result triggers a // textDocument/didOpen without a corresponding textDocument/didClose. func TestClearAnalysisDiagnostics(t *testing.T) { - withOptions( + WithOptions( ProxyFiles(workspaceProxy), WorkspaceFolders("pkg/inner"), - ).run(t, workspaceModule, func(t *testing.T, env *Env) { + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { env.OpenFile("pkg/main.go") env.Await( env.DiagnosticAtRegexp("pkg/main2.go", "fmt.Print"), @@ -153,10 +158,10 @@ func TestClearAnalysisDiagnostics(t *testing.T) { // 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) { - withOptions( + WithOptions( ProxyFiles(workspaceProxy), WorkspaceFolders("pkg"), - ).run(t, workspaceModule, func(t *testing.T, env *Env) { + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { // Add a replace directive and expect the files that gopls is watching // to change. dir := env.Sandbox.Workdir.URI("goodbye").SpanURI().Filename() @@ -165,7 +170,7 @@ replace random.org => %s `, env.ReadWorkspaceFile("pkg/go.mod"), dir) env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace) env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), UnregistrationMatching("didChangeWatchedFiles"), RegistrationMatching("didChangeWatchedFiles"), ) @@ -199,7 +204,9 @@ func TestAutomaticWorkspaceModule_Interdependent(t *testing.T) { 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 @@ -221,10 +228,10 @@ func Hello() int { var x int } ` - withOptions( + WithOptions( ProxyFiles(workspaceModuleProxy), Modes(Experimental), - ).run(t, multiModule, func(t *testing.T, env *Env) { + ).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"), @@ -241,7 +248,9 @@ func TestDeleteModule_Interdependent(t *testing.T) { 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 @@ -263,10 +272,10 @@ func Hello() int { var x int } ` - withOptions( + WithOptions( ProxyFiles(workspaceModuleProxy), Modes(Experimental), - ).run(t, multiModule, func(t *testing.T, env *Env) { + ).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")) @@ -277,7 +286,7 @@ func Hello() int { env.RemoveWorkspaceFile("modb/b/b.go") env.RemoveWorkspaceFile("modb/go.mod") env.Await( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2), + env.DoneWithChangeWatchedFiles(), ) if testenv.Go1Point() < 14 { // On 1.14 and above, the go mod tidy diagnostics accidentally @@ -306,7 +315,9 @@ func TestCreateModule_Interdependent(t *testing.T) { 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 @@ -319,10 +330,10 @@ func main() { _ = b.Hello() } ` - withOptions( + WithOptions( Modes(Experimental), ProxyFiles(workspaceModuleProxy), - ).run(t, multiModule, func(t *testing.T, env *Env) { + ).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")) if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) { @@ -340,7 +351,7 @@ func Hello() int { }) env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), + env.DoneWithChangeWatchedFiles(), env.DiagnosticAtRegexp("modb/b/b.go", "x"), ), ) @@ -381,14 +392,14 @@ func Hello() int { var x int } ` - withOptions( + WithOptions( ProxyFiles(workspaceModuleProxy), Modes(Experimental), - ).run(t, multiModule, func(t *testing.T, env *Env) { + ).Run(t, multiModule, func(t *testing.T, env *Env) { env.OpenFile("modb/go.mod") env.Await( OnceMet( - CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1), + env.DoneWithOpen(), DiagnosticAt("modb/go.mod", 0, 0), ), ) @@ -409,7 +420,9 @@ func TestUseGoplsMod(t *testing.T) { 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 @@ -425,6 +438,9 @@ func main() { 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/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= -- modb/b/b.go -- package b @@ -441,10 +457,10 @@ require ( replace a.com => $SANDBOX_WORKDIR/moda/a ` - withOptions( + WithOptions( ProxyFiles(workspaceModuleProxy), Modes(Experimental), - ).run(t, multiModule, func(t *testing.T, env *Env) { + ).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. @@ -472,8 +488,8 @@ replace a.com => $SANDBOX_WORKDIR/moda/a env.WriteWorkspaceFile("gopls.mod", fmt.Sprintf(`module gopls-workspace require ( - a.com v0.0.0-goplsworkspace - b.com v0.0.0-goplsworkspace + a.com v1.9999999.0-goplsworkspace + b.com v1.9999999.0-goplsworkspace ) replace a.com => %s/moda/a @@ -488,7 +504,7 @@ replace b.com => %s/modb var d protocol.PublishDiagnosticsParams env.Await( OnceMet( - env.DiagnosticAtRegexp("modb/go.mod", `require example.com v1.2.3`), + env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"), ReadDiagnostics("modb/go.mod", &d), ), ) @@ -547,7 +563,7 @@ package foo import "fmt" var _ = fmt.Printf ` - run(t, files, func(t *testing.T, env *Env) { + 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`)) @@ -598,9 +614,9 @@ func main() { var x int } ` - withOptions( + WithOptions( Modes(Experimental), - ).run(t, multiModule, func(t *testing.T, env *Env) { + ).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"), @@ -630,10 +646,10 @@ func main() { fmt.Println("World") } ` - withOptions( + WithOptions( Modes(Experimental), SendPID(), - ).run(t, multiModule, func(t *testing.T, env *Env) { + ).Run(t, multiModule, func(t *testing.T, env *Env) { pid := os.Getpid() // Don't factor this out of Server.addFolders. vscode-go expects this // directory. @@ -643,7 +659,7 @@ func main() { t.Fatalf("reading expected workspace modfile: %v", err) } got := string(gotb) - for _, want := range []string{"a.com v0.0.0-goplsworkspace", "b.com v0.0.0-goplsworkspace"} { + for _, want := range []string{"a.com v1.9999999.0-goplsworkspace", "b.com v1.9999999.0-goplsworkspace"} { if !strings.Contains(got, want) { // want before got here, since the go.mod is multi-line t.Fatalf("workspace go.mod missing %q. got:\n%s", want, got) @@ -654,18 +670,18 @@ func main() { module gopls-workspace require ( - a.com v0.0.0-goplsworkspace + a.com v1.9999999.0-goplsworkspace ) replace a.com => %s/moda/a `, workdir)) - env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1)) + env.Await(env.DoneWithChangeWatchedFiles()) gotb, err = ioutil.ReadFile(modPath) if err != nil { t.Fatalf("reading expected workspace modfile: %v", err) } got = string(gotb) - want := "b.com v0.0.0-goplsworkspace" + want := "b.com v1.9999999.0-goplsworkspace" if strings.Contains(got, want) { t.Fatalf("workspace go.mod contains unexpected %q. got:\n%s", want, got) } @@ -687,7 +703,7 @@ const _ = Nonexistant cfg := EditorConfig{ DirectoryFilters: []string{"-exclude"}, } - withOptions(cfg).run(t, files, func(t *testing.T, env *Env) { + WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) { env.Await(NoDiagnostics("exclude/x.go")) }) } @@ -715,7 +731,7 @@ const X = 1 cfg := EditorConfig{ DirectoryFilters: []string{"-exclude"}, } - withOptions(cfg).run(t, files, func(t *testing.T, env *Env) { + WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) { env.Await( NoDiagnostics("exclude/exclude.go"), // filtered out NoDiagnostics("include/include.go"), // successfully builds @@ -765,7 +781,7 @@ package exclude cfg := EditorConfig{ DirectoryFilters: []string{"-exclude"}, } - withOptions(cfg, Modes(Experimental), ProxyFiles(proxy)).run(t, files, func(t *testing.T, env *Env) { + WithOptions(cfg, Modes(Experimental), ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) { env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`)) }) } diff --git a/gopls/internal/regtest/wrappers.go b/gopls/internal/regtest/wrappers.go index cdc30900d15..bb614a5ed5d 100644 --- a/gopls/internal/regtest/wrappers.go +++ b/gopls/internal/regtest/wrappers.go @@ -361,7 +361,7 @@ func (e *Env) CodeAction(path string) []protocol.CodeAction { return actions } -func (e *Env) changeConfiguration(t *testing.T, config *fake.EditorConfig) { +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 diff --git a/internal/gocommand/vendor.go b/internal/gocommand/vendor.go index 1cd8d8473e9..5e75bd6d8fa 100644 --- a/internal/gocommand/vendor.go +++ b/internal/gocommand/vendor.go @@ -12,6 +12,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "golang.org/x/mod/semver" ) @@ -19,11 +20,15 @@ import ( // ModuleJSON holds information about a module. type ModuleJSON struct { Path string // module path + Version string // module version + Versions []string // available module versions (with -versions) Replace *ModuleJSON // replaced by this module + Time *time.Time // time version was created + Update *ModuleJSON // available update, if any (with -u) Main bool // is this the main module? Indirect bool // is this module only an indirect dependency of main module? Dir string // directory holding files for this module, if any - GoMod string // path to go.mod file for this module, if any + GoMod string // path to go.mod file used when loading this module, if any GoVersion string // go version used in module } diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index fb328b8bdaa..41bc79d15f8 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -203,8 +203,8 @@ func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalErr 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 (https://github.com/golang/go/issues/32394), -and you can learn more here: https://github.com/golang/go/issues/36899.` +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: errors.Errorf(msg), ErrorList: s.applyCriticalErrorToFiles(ctx, msg, openFiles), @@ -236,13 +236,15 @@ and you can learn more here: https://github.com/golang/go/issues/36899.` 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/go/issues/36899. +You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md. `, modDir, filepath.Dir(rootModURI.Filename()), modDir) srcErrs = append(srcErrs, s.applyCriticalErrorToFiles(ctx, msg, uris)...) } if len(srcErrs) != 0 { return &source.CriticalError{ - MainError: errors.Errorf(`You are working in a nested module. Please open it as a separate workspace folder.`), + MainError: errors.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.`), ErrorList: srcErrs, } } diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index cfd3fd1734b..7949918dfe6 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -6,14 +6,10 @@ package cache import ( "context" - "encoding/json" "fmt" - "io" - "os" "path/filepath" "regexp" "strings" - "unicode" "golang.org/x/mod/modfile" "golang.org/x/mod/module" @@ -220,125 +216,6 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string return mwh.why(ctx, s) } -type modUpgradeHandle struct { - handle *memoize.Handle -} - -type modUpgradeData struct { - // upgrades maps modules to their latest versions. - upgrades map[string]string - - err error -} - -func (muh *modUpgradeHandle) upgrades(ctx context.Context, snapshot *snapshot) (map[string]string, error) { - v, err := muh.handle.Get(ctx, snapshot.generation, snapshot) - if v == nil { - return nil, err - } - data := v.(*modUpgradeData) - return data.upgrades, data.err -} - -// moduleUpgrade describes a module that can be upgraded to a particular -// version. -type moduleUpgrade struct { - Path string - Update struct { - Version string - } -} - -func (s *snapshot) ModUpgrade(ctx context.Context, fh source.FileHandle) (map[string]string, error) { - if fh.Kind() != source.Mod { - return nil, fmt.Errorf("%s is not a go.mod file", fh.URI()) - } - if handle := s.getModUpgradeHandle(fh.URI()); handle != nil { - return handle.upgrades(ctx, s) - } - key := modKey{ - sessionID: s.view.session.id, - env: hashEnv(s), - mod: fh.FileIdentity(), - view: s.view.rootURI.Filename(), - verb: upgrade, - } - h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { - ctx, done := event.Start(ctx, "cache.ModUpgradeHandle", tag.URI.Of(fh.URI())) - defer done() - - snapshot := arg.(*snapshot) - - pm, err := snapshot.ParseMod(ctx, fh) - if err != nil { - return &modUpgradeData{err: err} - } - - // No requires to upgrade. - if len(pm.File.Require) == 0 { - return &modUpgradeData{} - } - // Run "go list -mod readonly -u -m all" to be able to see which deps can be - // upgraded without modifying mod file. - inv := &gocommand.Invocation{ - Verb: "list", - Args: []string{"-u", "-m", "-json", "all"}, - WorkingDir: filepath.Dir(fh.URI().Filename()), - } - if s.workspaceMode()&tempModfile == 0 || containsVendor(fh.URI()) { - // Use -mod=readonly if the module contains a vendor directory - // (see golang/go#38711). - inv.ModFlag = "readonly" - } - stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, inv) - if err != nil { - return &modUpgradeData{err: err} - } - var upgradeList []moduleUpgrade - dec := json.NewDecoder(stdout) - for { - var m moduleUpgrade - if err := dec.Decode(&m); err == io.EOF { - break - } else if err != nil { - return &modUpgradeData{err: err} - } - upgradeList = append(upgradeList, m) - } - if len(upgradeList) <= 1 { - return &modUpgradeData{} - } - upgrades := make(map[string]string) - for _, upgrade := range upgradeList[1:] { - if upgrade.Update.Version == "" { - continue - } - upgrades[upgrade.Path] = upgrade.Update.Version - } - return &modUpgradeData{ - upgrades: upgrades, - } - }, nil) - muh := &modUpgradeHandle{handle: h} - s.mu.Lock() - s.modUpgradeHandles[fh.URI()] = muh - s.mu.Unlock() - - return muh.upgrades(ctx, s) -} - -// containsVendor reports whether the module has a vendor folder. -func containsVendor(modURI span.URI) bool { - dir := filepath.Dir(modURI.Filename()) - f, err := os.Stat(filepath.Join(dir, "vendor")) - if err != nil { - return false - } - return f.IsDir() -} - -var moduleAtVersionRe = regexp.MustCompile(`^(?P.*)@(?P.*)$`) - // extractGoCommandError tries to parse errors that come from the go command // and shape them into go.mod diagnostics. func (s *snapshot) extractGoCommandErrors(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, goCmdError string) []*source.Error { @@ -356,6 +233,8 @@ func (s *snapshot) extractGoCommandErrors(ctx context.Context, snapshot source.S return srcErrs } +var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`) + // matchErrorToModule attempts to match module version in error messages. // Some examples: // @@ -363,99 +242,99 @@ func (s *snapshot) extractGoCommandErrors(ctx context.Context, snapshot source.S // go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72 // go: example.com@v1.2.3 requires\n\trandom.org@v1.2.3: parsing go.mod:\n\tmodule declares its path as: bob.org\n\tbut was required as: random.org // -// We split on colons and whitespace, and attempt to match on something -// that matches module@version. If we're able to find a match, we try to -// find anything that matches it in the go.mod file. +// We search for module@version, starting from the end to find the most +// relevant module, e.g. random.org@v1.2.3 above. Then we associate the error +// with a directive that references any of the modules mentioned. func (s *snapshot) matchErrorToModule(ctx context.Context, fh source.FileHandle, goCmdError string) *source.Error { - var v module.Version - fields := strings.FieldsFunc(goCmdError, func(r rune) bool { - return unicode.IsSpace(r) || r == ':' - }) - for _, field := range fields { - match := moduleAtVersionRe.FindStringSubmatch(field) - if match == nil { - continue - } - path, version := match[1], match[2] + pm, err := s.ParseMod(ctx, fh) + if err != nil { + return nil + } + + var innermost *module.Version + var reference *modfile.Line + matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) + +outer: + 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(version) { + if source.IsWorkspaceModuleVersion(ver.Version) { continue } - if err := module.Check(path, version); err != nil { + if err := module.Check(ver.Path, ver.Version); err != nil { continue } - v.Path, v.Version = path, version - break + if innermost == nil { + innermost = &ver + } + + for _, req := range pm.File.Require { + if req.Mod == ver { + reference = req.Syntax + break outer + } + } + for _, ex := range pm.File.Exclude { + if ex.Mod == ver { + reference = ex.Syntax + break outer + } + } + for _, rep := range pm.File.Replace { + if rep.New == ver || rep.Old == ver { + reference = rep.Syntax + break outer + } + } } - pm, err := s.ParseMod(ctx, fh) + + if reference == nil { + // No match for the module path was found in the go.mod file. + // Show the error on the module declaration, if one exists. + if pm.File.Module == nil { + return nil + } + reference = pm.File.Module.Syntax + } + + rng, err := rangeFromPositions(pm.Mapper, reference.Start, reference.End) if err != nil { return nil } - toSourceError := func(line *modfile.Line) *source.Error { - rng, err := rangeFromPositions(pm.Mapper, line.Start, line.End) + disabledByGOPROXY := strings.Contains(goCmdError, "disabled by GOPROXY=off") + shouldAddDep := strings.Contains(goCmdError, "to add it") + if innermost != nil && (disabledByGOPROXY || shouldAddDep) { + args, err := source.MarshalArgs(fh.URI(), false, []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)}) if err != nil { return nil } - disabledByGOPROXY := strings.Contains(goCmdError, "disabled by GOPROXY=off") - shouldAddDep := strings.Contains(goCmdError, "to add it") - if v.Path != "" && (disabledByGOPROXY || shouldAddDep) { - args, err := source.MarshalArgs(fh.URI(), false, []string{fmt.Sprintf("%v@%v", v.Path, v.Version)}) - if err != nil { - return nil - } - msg := goCmdError - if disabledByGOPROXY { - msg = fmt.Sprintf("%v@%v has not been downloaded", v.Path, v.Version) - } - return &source.Error{ - Message: msg, - Kind: source.ListError, - Range: rng, - URI: fh.URI(), - SuggestedFixes: []source.SuggestedFix{{ - Title: fmt.Sprintf("Download %v@%v", v.Path, v.Version), - Command: &protocol.Command{ - Title: source.CommandAddDependency.Title, - Command: source.CommandAddDependency.ID(), - Arguments: args, - }, - }}, - } + msg := goCmdError + if disabledByGOPROXY { + msg = fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version) } return &source.Error{ - Message: goCmdError, + Message: msg, + Kind: source.ListError, Range: rng, URI: fh.URI(), - Kind: source.ListError, - } - } - // Check if there are any require, exclude, or replace statements that - // match this module version. - for _, req := range pm.File.Require { - if req.Mod != v { - continue - } - return toSourceError(req.Syntax) - } - for _, ex := range pm.File.Exclude { - if ex.Mod != v { - continue - } - return toSourceError(ex.Syntax) - } - for _, rep := range pm.File.Replace { - if rep.New != v && rep.Old != v { - continue + SuggestedFixes: []source.SuggestedFix{{ + Title: fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version), + Command: &protocol.Command{ + Title: source.CommandAddDependency.Title, + Command: source.CommandAddDependency.ID(), + Arguments: args, + }, + }}, } - return toSourceError(rep.Syntax) } - // No match for the module path was found in the go.mod file. - // Show the error on the module declaration, if one exists. - if pm.File.Module == nil { - return nil + return &source.Error{ + Message: goCmdError, + Range: rng, + URI: fh.URI(), + Kind: source.ListError, } - return toSourceError(pm.File.Module.Syntax) } // errorPositionRe matches errors messages of the form ::, diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 552d1c5830e..11028083f1e 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -200,8 +200,9 @@ func (s *Session) createView(ctx context.Context, name string, folder, tempWorks baseCtx: baseCtx, name: name, folder: folder, - filesByURI: make(map[span.URI]*fileBase), - filesByBase: make(map[string][]*fileBase), + moduleUpgrades: map[string]string{}, + filesByURI: map[span.URI]*fileBase{}, + filesByBase: map[string][]*fileBase{}, rootURI: root, workspaceInformation: *ws, tempWorkspace: tempWorkspace, @@ -230,7 +231,6 @@ func (s *Session) createView(ctx context.Context, name string, folder, tempWorks unloadableFiles: make(map[span.URI]struct{}), parseModHandles: make(map[span.URI]*parseModHandle), modTidyHandles: make(map[span.URI]*modTidyHandle), - modUpgradeHandles: make(map[span.URI]*modUpgradeHandle), modWhyHandles: make(map[span.URI]*modWhyHandle), workspace: workspace, } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 08459a747cb..7bea58e2a3e 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -21,6 +21,7 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/mod/module" + "golang.org/x/mod/semver" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" @@ -103,9 +104,8 @@ type snapshot struct { // 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 - modUpgradeHandles map[span.URI]*modUpgradeHandle - modWhyHandles map[span.URI]*modWhyHandle + modTidyHandles map[span.URI]*modTidyHandle + modWhyHandles map[span.URI]*modWhyHandle workspace *workspace workspaceDirHandle *memoize.Handle @@ -324,12 +324,8 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat case source.LoadWorkspace, source.Normal: if vendorEnabled { inv.ModFlag = "vendor" - } else if s.workspaceMode()&usesWorkspaceModule == 0 && !allowModfileModificationOption { + } else if !allowModfileModificationOption { inv.ModFlag = "readonly" - } else { - // Temporarily allow updates for multi-module workspace mode: - // it doesn't create a go.sum at all. golang/go#42509. - inv.ModFlag = mutableModFlag } case source.UpdateUserModFile, source.WriteTemporaryModFile: inv.ModFlag = mutableModFlag @@ -574,12 +570,6 @@ func (s *snapshot) getModWhyHandle(uri span.URI) *modWhyHandle { return s.modWhyHandles[uri] } -func (s *snapshot) getModUpgradeHandle(uri span.URI) *modUpgradeHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.modUpgradeHandles[uri] -} - func (s *snapshot) getModTidyHandle(uri span.URI) *modTidyHandle { s.mu.Lock() defer s.mu.Unlock() @@ -717,6 +707,9 @@ func (s *snapshot) allKnownSubdirs(ctx context.Context) map[span.URI]struct{} { // the given directory. It does not respect symlinks. func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI { var files []span.URI + s.mu.Lock() + defer s.mu.Unlock() + for uri := range s.files { if source.InDir(dir.Filename(), uri.Filename()) { files = append(files, uri) @@ -1009,6 +1002,9 @@ func (s *snapshot) awaitLoaded(ctx context.Context) error { func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { loadErr := s.awaitLoadedAllErrors(ctx) + if errors.Is(loadErr, context.Canceled) { + return nil + } // Even if packages didn't fail to load, we still may want to show // additional warnings. @@ -1075,6 +1071,10 @@ func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) error { // Do not return results until the snapshot's view has been initialized. s.AwaitInitialized(ctx) + if ctx.Err() != nil { + return ctx.Err() + } + if err := s.reloadWorkspace(ctx); err != nil { return err } @@ -1286,7 +1286,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC unloadableFiles: make(map[span.URI]struct{}), parseModHandles: make(map[span.URI]*parseModHandle), modTidyHandles: make(map[span.URI]*modTidyHandle), - modUpgradeHandles: make(map[span.URI]*modUpgradeHandle), modWhyHandles: make(map[span.URI]*modWhyHandle), workspace: newWorkspace, } @@ -1331,12 +1330,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } result.modTidyHandles[k] = v } - for k, v := range s.modUpgradeHandles { - if _, ok := changes[k]; ok { - continue - } - result.modUpgradeHandles[k] = v - } for k, v := range s.modWhyHandles { if _, ok := changes[k]; ok { continue @@ -1390,9 +1383,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC for k := range s.modTidyHandles { delete(result.modTidyHandles, k) } - for k := range s.modUpgradeHandles { - delete(result.modUpgradeHandles, k) - } for k := range s.modWhyHandles { delete(result.modWhyHandles, k) } @@ -1521,9 +1511,6 @@ copyIDs: for _, v := range result.modTidyHandles { newGen.Inherit(v.handle) } - for _, v := range result.modUpgradeHandles { - newGen.Inherit(v.handle) - } for _, v := range result.modWhyHandles { newGen.Inherit(v.handle) } @@ -1731,6 +1718,9 @@ func BuildGoplsMod(ctx context.Context, root span.URI, s source.Snapshot) (*modf 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 := make(map[string]span.URI) for modURI := range modFiles { @@ -1749,7 +1739,13 @@ func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, if file == nil || parsed.Module == nil { return nil, fmt.Errorf("no module declaration for %s", modURI) } + if parsed.Go != nil && semver.Compare(goVersion, parsed.Go.Version) < 0 { + 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) + } paths[path] = modURI // If the module's path includes a major version, we expect it to have // a matching major version. @@ -1763,6 +1759,9 @@ func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, return nil, err } } + if goVersion != "" { + file.AddGoStmt(goVersion) + } // Go back through all of the modules to handle any of their replace // statements. for modURI := range modFiles { @@ -1802,6 +1801,7 @@ func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, } } } + file.SortBlocks() return file, nil } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index ec355619753..5b767d11575 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -60,6 +60,9 @@ type View struct { importsState *importsState + // moduleUpgrades tracks known upgrades for module paths. + moduleUpgrades 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 filesByURI map[span.URI]*fileBase @@ -863,6 +866,26 @@ func (v *View) IsGoPrivatePath(target string) bool { return globsMatchPath(v.goprivate, target) } +func (v *View) ModuleUpgrades() map[string]string { + v.mu.Lock() + defer v.mu.Unlock() + + upgrades := map[string]string{} + for mod, ver := range v.moduleUpgrades { + upgrades[mod] = ver + } + return upgrades +} + +func (v *View) RegisterModuleUpgrades(upgrades map[string]string) { + v.mu.Lock() + defer v.mu.Unlock() + + for mod, ver := range upgrades { + v.moduleUpgrades[mod] = ver + } +} + // 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/internal/lsp/cmd/call_hierarchy.go b/internal/lsp/cmd/call_hierarchy.go index 48b4c46e6de..2f870f0c72f 100644 --- a/internal/lsp/cmd/call_hierarchy.go +++ b/internal/lsp/cmd/call_hierarchy.go @@ -30,8 +30,6 @@ Example: $ # 1-indexed location (:line:column or :#offset) of the target identifier $ gopls call_hierarchy helper/helper.go:8:6 $ gopls call_hierarchy helper/helper.go:#53 - - gopls call_hierarchy flags are: `) f.PrintDefaults() } diff --git a/internal/lsp/cmd/check.go b/internal/lsp/cmd/check.go index 7d22db8d3bf..42d1976ec37 100644 --- a/internal/lsp/cmd/check.go +++ b/internal/lsp/cmd/check.go @@ -26,8 +26,6 @@ func (c *check) DetailedHelp(f *flag.FlagSet) { Example: show the diagnostic results of this file: $ gopls check internal/lsp/cmd/check.go - - gopls check flags are: `) f.PrintDefaults() } diff --git a/internal/lsp/cmd/highlight.go b/internal/lsp/cmd/highlight.go index f2f380613f4..b60d5134526 100644 --- a/internal/lsp/cmd/highlight.go +++ b/internal/lsp/cmd/highlight.go @@ -30,8 +30,6 @@ Example: $ # 1-indexed location (:line:column or :#offset) of the target identifier $ gopls highlight helper/helper.go:8:6 $ gopls highlight helper/helper.go:#53 - - gopls highlight flags are: `) f.PrintDefaults() } diff --git a/internal/lsp/cmd/implementation.go b/internal/lsp/cmd/implementation.go index e498372143a..18eaa4ed3f4 100644 --- a/internal/lsp/cmd/implementation.go +++ b/internal/lsp/cmd/implementation.go @@ -30,8 +30,6 @@ Example: $ # 1-indexed location (:line:column or :#offset) of the target identifier $ gopls implementation helper/helper.go:8:6 $ gopls implementation helper/helper.go:#53 - - gopls implementation flags are: `) f.PrintDefaults() } diff --git a/internal/lsp/cmd/prepare_rename.go b/internal/lsp/cmd/prepare_rename.go index 2a2fffec254..2e6965e9115 100644 --- a/internal/lsp/cmd/prepare_rename.go +++ b/internal/lsp/cmd/prepare_rename.go @@ -30,8 +30,6 @@ Example: $ # 1-indexed location (:line:column or :#offset) of the target identifier $ gopls prepare_rename helper/helper.go:8:6 $ gopls prepare_rename helper/helper.go:#53 - - gopls prepare_rename flags are: `) f.PrintDefaults() } diff --git a/internal/lsp/cmd/semantictokens.go b/internal/lsp/cmd/semantictokens.go index 7716d14d630..cf7d43145a8 100644 --- a/internal/lsp/cmd/semantictokens.go +++ b/internal/lsp/cmd/semantictokens.go @@ -67,8 +67,6 @@ func (c *semtok) DetailedHelp(f *flag.FlagSet) { Example: show the semantic tokens for this file: $ gopls semtok internal/lsp/cmd/semtok.go - - gopls semtok flags are: `) f.PrintDefaults() } diff --git a/internal/lsp/cmd/signature.go b/internal/lsp/cmd/signature.go index c628f381fa3..0a7a599c971 100644 --- a/internal/lsp/cmd/signature.go +++ b/internal/lsp/cmd/signature.go @@ -29,8 +29,6 @@ Example: $ # 1-indexed location (:line:column or :#offset) of the target identifier $ gopls signature helper/helper.go:8:6 $ gopls signature helper/helper.go:#53 - - gopls signature flags are: `) f.PrintDefaults() } diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 79c866331e3..a3bc21dd6a4 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -217,6 +217,25 @@ func (s *Server) runCommand(ctx context.Context, work *workDone, command *source return err } return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"}) + case source.CommandCheckUpgrades: + var uri protocol.DocumentURI + var modules []string + if err := source.UnmarshalArgs(args, &uri, &modules); err != nil { + return err + } + snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind) + defer release() + if !ok { + return err + } + upgrades, err := s.getUpgrades(ctx, snapshot, uri.SpanURI(), modules) + if err != nil { + return err + } + snapshot.View().RegisterModuleUpgrades(upgrades) + // Re-diagnose the snapshot to publish the new module diagnostics. + s.diagnoseSnapshot(snapshot, nil, false) + return nil case source.CommandAddDependency, source.CommandUpgradeDependency: var uri protocol.DocumentURI var goCmdArgs []string @@ -511,3 +530,27 @@ func runSimpleGoCommand(ctx context.Context, snapshot source.Snapshot, mode sour }) return err } + +func (s *Server) getUpgrades(ctx context.Context, snapshot source.Snapshot, uri span.URI, modules []string) (map[string]string, error) { + stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{ + Verb: "list", + Args: append([]string{"-m", "-u", "-json"}, modules...), + WorkingDir: filepath.Dir(uri.Filename()), + }) + if err != nil { + return nil, err + } + + upgrades := map[string]string{} + for dec := json.NewDecoder(stdout); dec.More(); { + mod := &gocommand.ModuleJSON{} + if err := dec.Decode(mod); err != nil { + return nil, err + } + if mod.Update == nil { + continue + } + upgrades[mod.Path] = mod.Update.Version + } + return upgrades, nil +} diff --git a/internal/lsp/debug/info.go b/internal/lsp/debug/info.go index 9027b0067df..8aba55241c4 100644 --- a/internal/lsp/debug/info.go +++ b/internal/lsp/debug/info.go @@ -26,7 +26,7 @@ const ( ) // Version is a manually-updated mechanism for tracking versions. -const Version = "v0.6.4" +const Version = "v0.6.5" // ServerVersion is the format used by gopls to report its version to the // client. This format is structured so that the client can parse it easily. diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go index aec976ad6fd..473518e4337 100644 --- a/internal/lsp/debug/serve.go +++ b/internal/lsp/debug/serve.go @@ -181,6 +181,7 @@ type Client struct { Logfile string GoplsPath string ServerID string + Service protocol.Server } // A Server is an outgoing connection to a remote LSP server. @@ -314,6 +315,16 @@ func (i *Instance) getInfo(r *http.Request) interface{} { return template.HTML(buf.String()) } +func (i *Instance) AddService(s protocol.Server, session *cache.Session) { + for _, c := range i.State.clients { + if c.Session == session { + c.Service = s + return + } + } + stdlog.Printf("unable to find a Client to add the protocol.Server to") +} + func getMemory(r *http.Request) interface{} { var m runtime.MemStats runtime.ReadMemStats(&m) @@ -785,6 +796,29 @@ Using session: {{template "sessionlink" .Session.ID}}
{{if .DebugAddress}}Debug this client at: {{localAddress .DebugAddress}}
{{end}} Logfile: {{.Logfile}}
Gopls Path: {{.GoplsPath}}
+

Diagnostics

+{{/*Service: []protocol.Server; each server has map[uri]fileReports; + each fileReport: map[diagnosticSoure]diagnosticReport + diagnosticSource is one of 5 source + diagnosticReport: snapshotID and map[hash]*source.Diagnostic + sourceDiagnostic: struct { + Range protocol.Range + Message string + Source string + Code string + CodeHref string + Severity protocol.DiagnosticSeverity + Tags []protocol.DiagnosticTag + + Related []RelatedInformation + } + RelatedInformation: struct { + URI span.URI + Range protocol.Range + Message string + } + */}} +
    {{range $k, $v := .Service.Diagnostics}}
  • {{$k}}:
      {{range $v}}
    1. {{.}}
    2. {{end}}
  • {{end}}
{{end}} `)) diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 4941f007ea3..adf19ad86d8 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -50,6 +50,23 @@ type fileReports struct { reports map[diagnosticSource]diagnosticReport } +func (d diagnosticSource) String() string { + switch d { + case modSource: + return "FromSource" + case gcDetailsSource: + return "FromGCDetails" + case analysisSource: + return "FromAnalysis" + case typeCheckSource: + return "FromTypeChecking" + case orphanedSource: + return "FromOrphans" + default: + return fmt.Sprintf("From?%d?", d) + } +} + // hashDiagnostics computes a hash to identify diags. func hashDiagnostics(diags ...*source.Diagnostic) string { source.SortDiagnostics(diags) @@ -546,3 +563,34 @@ func (s *Server) shouldIgnoreError(ctx context.Context, snapshot source.Snapshot }) return !hasGo } + +// Diagnostics formattedfor the debug server +// (all the relevant fields of Server are private) +// (The alternative is to export them) +func (s *Server) Diagnostics() map[string][]string { + ans := make(map[string][]string) + s.diagnosticsMu.Lock() + defer s.diagnosticsMu.Unlock() + for k, v := range s.diagnostics { + fn := k.Filename() + for typ, d := range v.reports { + if len(d.diags) == 0 { + continue + } + for _, dx := range d.diags { + ans[fn] = append(ans[fn], auxStr(dx, d, typ)) + } + } + } + return ans +} + +func auxStr(v *source.Diagnostic, d diagnosticReport, typ diagnosticSource) string { + // Tags? RelatedInformation? + msg := fmt.Sprintf("(%s)%q(source:%q,code:%q,severity:%s,snapshot:%d,type:%s)", + v.Range, v.Message, v.Source, v.Code, v.Severity, d.snapshotID, typ) + for _, r := range v.Related { + msg += fmt.Sprintf(" [%s:%s,%q]", r.URI.Filename(), r.Range, r.Message) + } + return msg +} diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index 758b64b4ed6..3df5b24de29 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -41,14 +41,17 @@ type Editor struct { buffers map[string]buffer // Capabilities / Options serverCapabilities protocol.ServerCapabilities + // Call metrics for the purpose of expectations. This is done in an ad-hoc // manner for now. Perhaps in the future we should do something more - // systematic. - calls CallCounts + // systematic. Guarded with a separate mutex as calls may need to be accessed + // asynchronously via callbacks into the Editor. + callsMu sync.Mutex + calls CallCounts } type CallCounts struct { - DidOpen, DidChange, DidChangeWatchedFiles int + DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64 } type buffer struct { @@ -140,8 +143,8 @@ func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHo } func (e *Editor) Stats() CallCounts { - e.mu.Lock() - defer e.mu.Unlock() + e.callsMu.Lock() + defer e.callsMu.Unlock() return e.calls } @@ -291,36 +294,48 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro return nil } +// 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) { if e.Server == nil { return } - 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 { - // Following VS Code, don't honor deletions or changes to dirty buffers. - if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted { - continue - } - content, err := e.sandbox.Workdir.ReadFile(evt.Path) - if err != nil { - continue // A race with some other operation. + // e may be locked when onFileChanges is called, but it is important that we + // synchronously increment this counter so that we can subsequently assert on + // the number of expected DidChangeWatchedFiles calls. + e.callsMu.Lock() + e.calls.DidChangeWatchedFiles++ + e.callsMu.Unlock() + + // Since e may be locked, we must run this mutation asynchronously. + 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 { + // Following VS Code, don't honor deletions or changes to dirty buffers. + if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted { + continue + } + + content, err := e.sandbox.Workdir.ReadFile(evt.Path) + if err != nil { + continue // A race with some other operation. + } + // During shutdown, this call will fail. Ignore the error. + _ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil) } - // During shutdown, this call will fail. Ignore the error. - _ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil) } - } - e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ - Changes: lspevts, - }) - e.calls.DidChangeWatchedFiles++ + e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ + Changes: lspevts, + }) + }() } // OpenFile creates a buffer for the given workdir-relative file. @@ -371,7 +386,9 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont }); err != nil { return errors.Errorf("DidOpen: %w", err) } + e.callsMu.Lock() e.calls.DidOpen++ + e.callsMu.Unlock() } return nil } @@ -393,6 +410,9 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error { }); err != nil { return errors.Errorf("DidClose: %w", err) } + e.callsMu.Lock() + e.calls.DidClose++ + e.callsMu.Unlock() } return nil } @@ -458,6 +478,9 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro if err := e.Server.DidSave(ctx, params); err != nil { return errors.Errorf("DidSave: %w", err) } + e.callsMu.Lock() + e.calls.DidSave++ + e.callsMu.Unlock() } return nil } @@ -653,7 +676,9 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty if err := e.Server.DidChange(ctx, params); err != nil { return errors.Errorf("DidChange: %w", err) } + e.callsMu.Lock() e.calls.DidChange++ + e.callsMu.Unlock() } return nil } diff --git a/internal/lsp/fake/workdir.go b/internal/lsp/fake/workdir.go index 3cc6f7343b6..5103bdb4e19 100644 --- a/internal/lsp/fake/workdir.go +++ b/internal/lsp/fake/workdir.go @@ -211,7 +211,7 @@ func (w *Workdir) sendEvents(ctx context.Context, evts []FileEvent) { copy(watchers, w.watchers) w.watcherMu.Unlock() for _, w := range watchers { - go w(ctx, evts) + w(ctx, evts) } } diff --git a/internal/lsp/fake/workdir_test.go b/internal/lsp/fake/workdir_test.go index 5c9a36cbc58..f57ea37e13b 100644 --- a/internal/lsp/fake/workdir_test.go +++ b/internal/lsp/fake/workdir_test.go @@ -41,7 +41,9 @@ func newWorkdir(t *testing.T) (*Workdir, <-chan []FileEvent, func()) { fileEvents := make(chan []FileEvent) watch := func(_ context.Context, events []FileEvent) { - fileEvents <- events + go func() { + fileEvents <- events + }() } wd.AddWatcher(watch) return wd, fileEvents, cleanup diff --git a/internal/lsp/lsprpc/lsprpc.go b/internal/lsp/lsprpc/lsprpc.go index 623533f22a3..51267910cfe 100644 --- a/internal/lsp/lsprpc/lsprpc.go +++ b/internal/lsp/lsprpc/lsprpc.go @@ -61,6 +61,7 @@ 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) } // Clients may or may not send a shutdown message. Make sure the server is // shut down. diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go index 4465637be27..88ffe844faa 100644 --- a/internal/lsp/mod/code_lens.go +++ b/internal/lsp/mod/code_lens.go @@ -42,6 +42,10 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File for _, req := range pm.File.Require { requires = append(requires, req.Mod.Path) } + checkUpgradeArgs, err := source.MarshalArgs(fh.URI(), requires) + if err != nil { + return nil, err + } upgradeDirectArgs, err := source.MarshalArgs(fh.URI(), false, requires) if err != nil { return nil, err @@ -51,7 +55,16 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File if err != nil { return nil, err } + return []protocol.CodeLens{ + { + Range: rng, + Command: protocol.Command{ + Title: "Check for upgrades", + Command: source.CommandCheckUpgrades.ID(), + Arguments: checkUpgradeArgs, + }, + }, { Range: rng, Command: protocol.Command{ @@ -69,7 +82,6 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File }, }, }, nil - } func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go index f57a743e432..b5da5605391 100644 --- a/internal/lsp/mod/diagnostics.go +++ b/internal/lsp/mod/diagnostics.go @@ -8,6 +8,7 @@ package mod import ( "context" + "fmt" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/debug/tag" @@ -36,9 +37,12 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.Vers Range: e.Range, Source: e.Category, } - if e.Category == "syntax" || e.Kind == source.ListError { + switch { + case e.Category == "syntax", e.Kind == source.ListError: d.Severity = protocol.SeverityError - } else { + case e.Kind == source.UpgradeNotification: + d.Severity = protocol.SeverityInformation + default: d.Severity = protocol.SeverityWarning } fh, err := snapshot.GetVersionedFile(ctx, e.URI) @@ -59,13 +63,49 @@ func ErrorsForMod(ctx context.Context, snapshot source.Snapshot, fh source.FileH } return pm.ParseErrors, nil } + + var errors []*source.Error + + // Add upgrade quick fixes for individual modules if we know about them. + upgrades := snapshot.View().ModuleUpgrades() + for _, req := range pm.File.Require { + ver, ok := upgrades[req.Mod.Path] + if !ok || req.Mod.Version == ver { + continue + } + rng, err := lineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End) + if err != nil { + return nil, err + } + // Upgrade to the exact version we offer the user, not the most recent. + args, err := source.MarshalArgs(fh.URI(), false, []string{req.Mod.Path + "@" + ver}) + if err != nil { + return nil, err + } + errors = append(errors, &source.Error{ + URI: fh.URI(), + Range: rng, + Kind: source.UpgradeNotification, + Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path), + SuggestedFixes: []source.SuggestedFix{{ + Title: fmt.Sprintf("Upgrade to %v", ver), + Command: &protocol.Command{ + Title: fmt.Sprintf("Upgrade to %v", ver), + Command: source.CommandUpgradeDependency.ID(), + Arguments: args, + }, + }}, + }) + } + tidied, err := snapshot.ModTidy(ctx, pm) if source.IsNonFatalGoModError(err) { - return nil, nil + return errors, nil } if err != nil { return nil, err } - return tidied.Errors, nil + errors = append(errors, tidied.Errors...) + return errors, nil } diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go index 09c19341752..340fae6712f 100644 --- a/internal/lsp/protocol/tsprotocol.go +++ b/internal/lsp/protocol/tsprotocol.go @@ -285,7 +285,7 @@ type CodeAction struct { * * @since 3.16.0 */ - Disabled struct { + Disabled *struct { /** * Human readable description of why the code action is currently disabled. * diff --git a/internal/lsp/rename.go b/internal/lsp/rename.go index cef0638ff54..5f27d23d1bd 100644 --- a/internal/lsp/rename.go +++ b/internal/lsp/rename.go @@ -43,9 +43,11 @@ func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRena } // Do not return errors here, as it adds clutter. // Returning a nil result means there is not a valid rename. - item, err := source.PrepareRename(ctx, snapshot, fh, params.Position) + item, usererr, err := source.PrepareRename(ctx, snapshot, fh, params.Position) if err != nil { - return nil, nil // ignore errors + // Return usererr here rather than err, to avoid cluttering the UI with + // internal error details. + return nil, usererr } // TODO(suzmue): return ident.Name as the placeholder text. return &item.Range, nil diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index da164ed8409..79c0ab9f8d7 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -712,6 +712,11 @@ var GeneratedAPIJSON = &APIJSON{ Title: "go get package", Doc: "go_get_package runs `go get` to fetch a package.\n", }, + { + Command: "gopls.check_upgrades", + Title: "Check for upgrades", + Doc: "check_upgrades checks for module upgrades.\n", + }, { Command: "gopls.add_dependency", Title: "Add dependency", diff --git a/internal/lsp/source/command.go b/internal/lsp/source/command.go index 16d57fff944..f014e136e86 100644 --- a/internal/lsp/source/command.go +++ b/internal/lsp/source/command.go @@ -65,6 +65,7 @@ var Commands = []*Command{ CommandUpdateGoSum, CommandUndeclaredName, CommandGoGetPackage, + CommandCheckUpgrades, CommandAddDependency, CommandUpgradeDependency, CommandRemoveDependency, @@ -113,6 +114,12 @@ var ( Title: "Update go.sum", } + // CommandCheckUpgrades checks for module upgrades. + CommandCheckUpgrades = &Command{ + Name: "check_upgrades", + Title: "Check for upgrades", + } + // CommandAddDependency adds a dependency. CommandAddDependency = &Command{ Name: "add_dependency", diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go index fdf3f63ec78..da7faf8f7d8 100644 --- a/internal/lsp/source/rename.go +++ b/internal/lsp/source/rename.go @@ -42,22 +42,30 @@ type PrepareItem struct { Text string } -func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (*PrepareItem, error) { +// PrepareRename searches for a valid renaming at position pp. +// +// The returned usererr is intended to be displayed to the user to explain why +// the prepare fails. Probably we could eliminate the redundancy in returning +// two errors, but for now this is done defensively. +func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) { ctx, done := event.Start(ctx, "source.PrepareRename") defer done() qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f, pp) if err != nil { - return nil, err + return nil, nil, err } node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg + if err := checkRenamable(obj); err != nil { + return nil, err, err + } mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End()) if err != nil { - return nil, err + return nil, nil, err } rng, err := mr.Range() if err != nil { - return nil, err + return nil, nil, err } if _, isImport := node.(*ast.ImportSpec); isImport { // We're not really renaming the import path. @@ -66,10 +74,22 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot return &PrepareItem{ Range: rng, Text: obj.Name(), - }, nil + }, nil, nil +} + +// checkRenamable verifies if an obj may be renamed. +func checkRenamable(obj types.Object) error { + if v, ok := obj.(*types.Var); ok && v.Embedded() { + return errors.New("can't rename embedded fields: rename the type directly or name the field") + } + if obj.Name() == "_" { + return errors.New("can't rename \"_\"") + } + return nil } -// Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package. +// Rename returns a map of TextEdits for each file modified when renaming a +// given identifier within a package. func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) { ctx, done := event.Start(ctx, "source.Rename") defer done() @@ -79,9 +99,11 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, return nil, err } - obj := qos[0].obj - pkg := qos[0].pkg + obj, pkg := qos[0].obj, qos[0].pkg + if err := checkRenamable(obj); err != nil { + return nil, err + } if obj.Name() == newName { return nil, errors.Errorf("old and new names are the same: %s", newName) } diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index bc670db5f52..5ad2ebcbc24 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -820,7 +820,7 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare if err != nil { t.Fatal(err) } - item, err := source.PrepareRename(r.ctx, r.snapshot, fh, srcRng.Start) + 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) diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 329c89ad4ca..7a576a6dac0 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -109,10 +109,6 @@ type Snapshot interface { // the given go.mod file. ModWhy(ctx context.Context, fh FileHandle) (map[string]string, error) - // ModUpgrade returns the possible updates for the module specified by the - // given go.mod file. - ModUpgrade(ctx context.Context, fh FileHandle) (map[string]string, error) - // ModTidy returns the results of `go mod tidy` for the module specified by // the given go.mod file. ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error) @@ -231,6 +227,12 @@ type View interface { // IsGoPrivatePath reports whether target is a private import path, as identified // by the GOPRIVATE environment variable. IsGoPrivatePath(path string) bool + + // ModuleUpgrades returns known module upgrades. + ModuleUpgrades() map[string]string + + // RegisterModuleUpgrades registers that upgrades exist for the given modules. + RegisterModuleUpgrades(upgrades map[string]string) } // A FileSource maps uris to FileHandles. This abstraction exists both for @@ -596,6 +598,7 @@ const ( TypeError ModTidyError Analysis + UpgradeNotification ) func (e *Error) Error() string { @@ -614,12 +617,22 @@ var ( // 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. -const workspaceModuleVersion = ".0.0-goplsworkspace" +// +// If workspace module A is dependent on workspace module B, we need our +// nonexistant 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/internal/lsp/testdata/good/good1.go b/internal/lsp/testdata/good/good1.go index bdccaed6363..c4664a7e5d4 100644 --- a/internal/lsp/testdata/good/good1.go +++ b/internal/lsp/testdata/good/good1.go @@ -1,7 +1,6 @@ package good //@diag("package", "no_diagnostics", "", "error") import ( - _ "go/ast" //@prepare("go/ast", "_", "_") "golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package") ) diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.golden b/internal/lsp/testdata/rename/issue43616/issue43616.go.golden index 367d52d3428..34d03ba7aa6 100644 --- a/internal/lsp/testdata/rename/issue43616/issue43616.go.golden +++ b/internal/lsp/testdata/rename/issue43616/issue43616.go.golden @@ -1,9 +1,13 @@ -- bar-rename -- package issue43616 -type bar int //@rename("foo","bar") +type bar int //@rename("foo","bar"),prepare("oo","foo","foo") -var x struct{ bar } +var x struct{ bar } //@rename("foo","baz") -var _ = x.bar +var _ = x.bar //@rename("foo","quux") +-- baz-rename -- +can't rename embedded fields: rename the type directly or name the field +-- quux-rename -- +can't rename embedded fields: rename the type directly or name the field diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.in b/internal/lsp/testdata/rename/issue43616/issue43616.go.in index 3686914c98a..aaad531b732 100644 --- a/internal/lsp/testdata/rename/issue43616/issue43616.go.in +++ b/internal/lsp/testdata/rename/issue43616/issue43616.go.in @@ -1,7 +1,7 @@ package issue43616 -type foo int //@rename("foo","bar") +type foo int //@rename("foo","bar"),prepare("oo","foo","foo") -var x struct{ foo } +var x struct{ foo } //@rename("foo","baz") -var _ = x.foo +var _ = x.foo //@rename("foo","quux") diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 5482a03c705..37bb48a98de 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -19,7 +19,7 @@ DefinitionsCount = 64 TypeDefinitionsCount = 2 HighlightsCount = 69 ReferencesCount = 25 -RenamesCount = 31 +RenamesCount = 33 PrepareRenamesCount = 7 SymbolsCount = 5 WorkspaceSymbolsCount = 20