[gopls-release-branch.0.6] all: merge master into gopls-release-branch.0.6
63754364 Revert "[gopls-release-branch.0.6] all: merge master into gopls-release-branch.0.6"
0af0626a internal/lsp: save all possible keys for analyses, codelenses
ee6d6aff internal/lsp: restructure user options (CL 278433 continued)
2152f4ed Merge "[gopls-release-branch.0.6] all: merge master into gopls-release-branch.0.6"
4a19ffb6 gopls/internal/regtest: await file changes in TestUseGoplsMod
481d425b [gopls-release-branch.0.6] all: merge master into gopls-release-branch.0.6
ef3185ba internal/lsp/cache: add a check for snapshot invariants
b8413747 internal/lsp: fix autocomplete appends on imports
929a8494 internal/lsp: restructure the way we report critical errors
fbbba25c gopls/doc: generate documentation for analyzers
84d76fe3 internal/lsp: fix unimported completions with -mod=readonly
0661ca7e gopls/internal/regtest: show line numbers if TestUseGoplsMod fails
13ff2212 internal/lsp/cmd: include new name in rename help message
0f6027f0 gopls/doc/vim.md: add table of contents, improve neovim docs
34cd474b internal/lsp: use an enum for GC annotations settings
b1c90890 internal/lsp/source: do not panic in "var func" outgoing callhierarchy
2b0845dc gopls/release: add a command to validate the gopls release process
57089f8f internal/lsp: remove dependencies using text edits when necessary
834755c7 internal/lsp: add tests to set configuration options
bdbb3c91 internal/lsp/cache: only reload the workspace on saved changes
3e0a2b75 gopls/internal/regtest: skip a some new builders where regtests time out
f6952e40 internal/lsp/cache: fix some package event tags
9cbb1efa internal/lsp/source: add the shadow analyzer
3fa0e8f8 gopls: disable TestTemplate on Android
f2e330f4 gopls/test: add type checking for debug server templates
19653561 internal/lsp/mod: fix misplaced code lens with individual requires
b57d1c5b internal/lsp: return an error if code action has no title
ae774e97 internal/lsp: don't show duplicate diagnostics for go.mod errors
5b06639e internal/lsp/source: only show "run file benchmarks" if available
11a5667e gopls/internal/regtest: test metadata validation only on save for go.mod
5b43ef93 go/analysis/passes/fieldalignment: filter comments from suggested fix
4b31ac34 gopls/internal/regtest: test that accepting fix removes diagnostics
008e4774 internal/lsp, gopls: recover from go-diff panics
48e5bd11 internal/lsp: add titles to `go mod tidy` and update go.sum fixes
fa10ef0b internal/lsp/source: update codelenses example
6307297f go/analysis/passes/fieldalignment: support fields without name
Change-Id: Ic66860e1eea3ce41e818011ec00b41a77cbd3345
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go
index ae56302..8b752be 100644
--- a/go/analysis/analysistest/analysistest.go
+++ b/go/analysis/analysistest/analysistest.go
@@ -195,7 +195,10 @@
continue
}
if want != string(formatted) {
- d := myers.ComputeEdits("", want, string(formatted))
+ d, err := myers.ComputeEdits("", want, string(formatted))
+ if err != nil {
+ t.Errorf("failed to compute suggested fixes: %v", err)
+ }
t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, d))
}
break
@@ -221,7 +224,10 @@
continue
}
if want != string(formatted) {
- d := myers.ComputeEdits("", want, string(formatted))
+ d, err := myers.ComputeEdits("", want, string(formatted))
+ if err != nil {
+ t.Errorf("failed to compute edits: %s", err)
+ }
t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(file.Name()+".golden", "actual", want, d))
}
}
diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go
index 6b86049..ca1bc53 100644
--- a/go/analysis/passes/fieldalignment/fieldalignment.go
+++ b/go/analysis/passes/fieldalignment/fieldalignment.go
@@ -76,7 +76,10 @@
// TODO: Preserve multi-named fields instead of flattening.
var flat []*ast.Field
for _, f := range node.Fields.List {
- if len(f.Names) == 1 {
+ // TODO: Preserve comment, for now get rid of them.
+ // See https://github.com/golang/go/issues/20744
+ f.Comment = 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 2a443a5..b47ee19 100644
--- a/go/analysis/passes/fieldalignment/testdata/src/a/a.go
+++ b/go/analysis/passes/fieldalignment/testdata/src/a/a.go
@@ -21,3 +21,17 @@
a uint32
b [0]byte
}
+
+type NoNameGood struct {
+ Good
+ y int32
+ x byte
+ z byte
+}
+
+type NoNameBad struct { // want "struct of size 20 could be 16"
+ Good
+ x byte
+ y int32
+ z byte
+}
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 76d8bb3..34fc21b 100644
--- a/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden
+++ b/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden
@@ -21,3 +21,17 @@
b [0]byte
a uint32
}
+
+type NoNameGood struct {
+ Good
+ y int32
+ x byte
+ z byte
+}
+
+type NoNameBad struct {
+ Good
+ y int32
+ x byte
+ z byte
+}
diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go b/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go
index d2020be..d6383b7 100644
--- a/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go
+++ b/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go
@@ -38,3 +38,11 @@
a3 [3]bool
_ [0]func()
}
+
+type Issue43233 struct { // want "struct with 88 pointer bytes could be 80"
+ AllowedEvents []*string // allowed events
+ BlockedEvents []*string // blocked events
+ APIVersion string `mapstructure:"api_version"`
+ BaseURL string `mapstructure:"base_url"`
+ AccessToken string `mapstructure:"access_token"`
+}
diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go.golden b/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go.golden
index 2c8bd28..c3aa6ba 100644
--- a/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go.golden
+++ b/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go.golden
@@ -39,3 +39,11 @@
a3 [3]bool
b bool
}
+
+type Issue43233 struct {
+ APIVersion string `mapstructure:"api_version"`
+ BaseURL string `mapstructure:"base_url"`
+ AccessToken string `mapstructure:"access_token"`
+ AllowedEvents []*string
+ BlockedEvents []*string
+}
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index dcfad5e..747b17e 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -1,64 +1,58 @@
# Analyzers
-<!--TODO: Generate this file from the documentation in golang/org/x/tools/go/analysis/passes and golang/org/x/tools/go/lsp/source/options.go.-->
-
This document describes the analyzers that `gopls` uses inside the editor.
-A value of `true` means that the analyzer is enabled by default and a value of `false` means it is disabled by default.
+Details about how to enable/disable these analyses can be found
+[here](settings.md#analyses).
-Details about how to enable/disable these analyses can be found [here](settings.md#analyses).
-
-## Go vet suite
-
-Below is the list of general analyzers that are used in `go vet`.
-
-### **asmdecl**
+<!-- BEGIN Analyzers: DO NOT MANUALLY EDIT THIS SECTION -->
+## **asmdecl**
report mismatches between assembly files and Go declarations
-Default value: `true`.
+**Enabled by default.**
-### **assign**
+## **assign**
check for useless assignments
-This checker reports assignments of the form `x = x` or `a[i] = a[i]`.
+This checker reports assignments of the form x = x or a[i] = a[i].
These are almost always useless, and even when they aren't they are
usually a mistake.
-Default value: `true`.
+**Enabled by default.**
-### **atomic**
+## **atomic**
check for common mistakes using the sync/atomic package
The atomic checker looks for assignment statements of the form:
-`x = atomic.AddUint64(&x, 1)`
+ x = atomic.AddUint64(&x, 1)
which are not atomic.
-Default value: `true`.
+**Enabled by default.**
-### **atomicalign**
+## **atomicalign**
check for non-64-bits-aligned arguments to sync/atomic functions
-Default value: `true`.
+**Enabled by default.**
-### **bools**
+## **bools**
check for common mistakes involving boolean operators
-Default value: `true`.
+**Enabled by default.**
-### **buildtag**
+## **buildtag**
check that +build tags are well-formed and correctly located
-Default value: `true`.
+**Enabled by default.**
-### **cgocall**
+## **cgocall**
detect some violations of the cgo pointer passing rules
@@ -69,9 +63,9 @@
Specifically, it warns about attempts to pass a Go chan, map, func,
or slice to C, either directly, or via a pointer, array, or struct.
-Default value: `true`.
+**Enabled by default.**
-### **composites**
+## **composites**
check for unkeyed composite literals
@@ -81,14 +75,17 @@
(even if unexported) to the struct will cause compilation to fail.
As an example,
-`err = &net.DNSConfigError{err}`
+
+ err = &net.DNSConfigError{err}
should be replaced by:
-`err = &net.DNSConfigError{Err: err}`
-Default value: `true`.
+ err = &net.DNSConfigError{Err: err}
-### **copylock**
+
+**Enabled by default.**
+
+## **copylocks**
check for locks erroneously passed by value
@@ -96,18 +93,41 @@
sync.WaitGroup, may cause both copies to malfunction. Generally such
values should be referred to through a pointer.
-Default value: `true`.
+**Enabled by default.**
-### **errorsas**
+## **deepequalerrors**
+
+check for calls of reflect.DeepEqual on error values
+
+The deepequalerrors checker looks for calls of the form:
+
+ reflect.DeepEqual(err1, err2)
+
+where err1 and err2 are errors. Using reflect.DeepEqual to compare
+errors is discouraged.
+
+**Enabled by default.**
+
+## **errorsas**
report passing non-pointer or non-error values to errors.As
The errorsas analysis reports calls to errors.As where the type
of the second argument is not a pointer to a type implementing error.
-Default value: `true`.
+**Enabled by default.**
-### **httpresponse**
+## **fieldalignment**
+
+find structs that would take less memory if their fields were sorted
+
+This analyzer find structs that can be rearranged to take less memory, and provides
+a suggested edit with the optimal order.
+
+
+**Disabled by default. Enable it by setting `"analyses": {"fieldalignment": true}`.**
+
+## **httpresponse**
check for mistakes using HTTP responses
@@ -115,21 +135,39 @@
call to close the http.Response Body before checking the error that
determines whether the response is valid:
-```go
-resp, err := http.Head(url)
-defer resp.Body.Close()
-if err != nil {
- log.Fatal(err)
-}
-// (defer statement belongs here)
-```
+ resp, err := http.Head(url)
+ defer resp.Body.Close()
+ if err != nil {
+ log.Fatal(err)
+ }
+ // (defer statement belongs here)
This checker helps uncover latent nil dereference bugs by reporting a
diagnostic for such mistakes.
-Default value: `true`.
+**Enabled by default.**
-### **loopclosure**
+## **ifaceassert**
+
+detect impossible interface-to-interface type assertions
+
+This checker flags type assertions v.(T) and corresponding type-switch cases
+in which the static type V of v is an interface that cannot possibly implement
+the target interface T. This occurs when V and T contain methods with the same
+name but different signatures. Example:
+
+ var v interface {
+ Read()
+ }
+ _ = v.(io.Reader)
+
+The Read method in v has a different signature than the Read method in
+io.Reader, so this assertion cannot succeed.
+
+
+**Enabled by default.**
+
+## **loopclosure**
check references to loop variables from within nested functions
@@ -140,19 +178,18 @@
program analysis.
For example:
-```go
-for i, v := range s {
- go func() {
- println(i, v) // not what you might expect
- }()
-}
-```
+
+ for i, v := range s {
+ go func() {
+ println(i, v) // not what you might expect
+ }()
+ }
See: https://golang.org/doc/go_faq.html#closures_and_goroutines
-Default value: `true`.
+**Enabled by default.**
-### **lostcancel**
+## **lostcancel**
check cancel func returned by context.WithCancel is called
@@ -161,17 +198,17 @@
until its parent context is cancelled.
(The background context is never cancelled.)
-Default value: `true`.
+**Enabled by default.**
-### **nilfunc**
+## **nilfunc**
check for useless comparisons between functions and nil
A useless comparison is one like f == nil as opposed to f() == nil.
-Default value: `true`.
+**Enabled by default.**
-### **printf**
+## **printf**
check consistency of Printf format strings and arguments
@@ -182,21 +219,17 @@
found by this analyzer's heuristics (for example, due to use of
dynamic calls) can insert a bogus call:
-```go
-if false {
- _ = fmt.Sprintf(format, args...) // enable printf checking
-}
-```
+ if false {
+ _ = fmt.Sprintf(format, args...) // enable printf checking
+ }
The -funcs flag specifies a comma-separated list of names of additional
known formatting functions or methods. If the name contains a period,
it must denote a specific function using one of the following forms:
-```
dir/pkg.Function
dir/pkg.Type.Method
(*dir/pkg.Type).Method
-```
Otherwise the name is interpreted as a case-insensitive unqualified
identifier such as "errorf". Either way, if a listed name ends in f, the
@@ -204,15 +237,99 @@
argument list. Otherwise it is assumed to be Print-like, taking a list
of arguments with no format string.
-Default value: `true`.
-### **shift**
+**Enabled by default.**
+
+## **shadow**
+
+check for possible unintended shadowing of variables
+
+This analyzer check for shadowed variables.
+A shadowed variable is a variable declared in an inner scope
+with the same name and type as a variable in an outer scope,
+and where the outer variable is mentioned after the inner one
+is declared.
+
+(This definition can be refined; the module generates too many
+false positives and is not yet enabled by default.)
+
+For example:
+
+ func BadRead(f *os.File, buf []byte) error {
+ var err error
+ for {
+ n, err := f.Read(buf) // shadows the function variable 'err'
+ if err != nil {
+ break // causes return of wrong value
+ }
+ foo(buf)
+ }
+ return err
+ }
+
+
+**Disabled by default. Enable it by setting `"analyses": {"shadow": true}`.**
+
+## **shift**
check for shifts that equal or exceed the width of the integer
-Default value: `true`.
+**Enabled by default.**
-### **stdmethods**
+## **simplifycompositelit**
+
+check for composite literal simplifications
+
+An array, slice, or map composite literal of the form:
+ []T{T{}, T{}}
+will be simplified to:
+ []T{{}, {}}
+
+This is one of the simplifications that "gofmt -s" applies.
+
+**Enabled by default.**
+
+## **simplifyrange**
+
+check for range statement simplifications
+
+A range of the form:
+ for x, _ = range v {...}
+will be simplified to:
+ for x = range v {...}
+
+A range of the form:
+ for _ = range v {...}
+will be simplified to:
+ for range v {...}
+
+This is one of the simplifications that "gofmt -s" applies.
+
+**Enabled by default.**
+
+## **simplifyslice**
+
+check for slice simplifications
+
+A slice expression of the form:
+ s[a:len(s)]
+will be simplified to:
+ s[a:]
+
+This is one of the simplifications that "gofmt -s" applies.
+
+**Enabled by default.**
+
+## **sortslice**
+
+check the argument type of sort.Slice
+
+sort.Slice requires an argument of a slice type. Check that
+the interface{} value passed to sort.Slice is actually a slice.
+
+**Enabled by default.**
+
+## **stdmethods**
check signature of methods of well-known interfaces
@@ -221,10 +338,8 @@
For example, the result of this WriteTo method should be (int64, error),
not error, to satisfy io.WriterTo:
-```go
type myWriterTo struct{...}
func (myWriterTo) WriteTo(w io.Writer) error { ... }
-```
This check ensures that each method whose name matches one of several
well-known interface methods from the standard library has the correct
@@ -236,17 +351,53 @@
UnmarshalJSON UnreadByte UnreadRune WriteByte
WriteTo
-Default value: `true`.
-### **structtag**
+**Enabled by default.**
+
+## **stringintconv**
+
+check for string(int) conversions
+
+This checker flags conversions of the form string(x) where x is an integer
+(but not byte or rune) type. Such conversions are discouraged because they
+return the UTF-8 representation of the Unicode code point x, and not a decimal
+string representation of x as one might expect. Furthermore, if x denotes an
+invalid code point, the conversion cannot be statically rejected.
+
+For conversions that intend on using the code point, consider replacing them
+with string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the
+string representation of the value in the desired base.
+
+
+**Enabled by default.**
+
+## **structtag**
check that struct field tags conform to reflect.StructTag.Get
Also report certain struct tags (json, xml) used with unexported fields.
-Default value: `true`.
+**Enabled by default.**
-### **tests**
+## **testinggoroutine**
+
+report calls to (*testing.T).Fatal from goroutines started by a test.
+
+Functions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and
+Skip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.
+This checker detects calls to these functions that occur within a goroutine
+started by the test. For example:
+
+func TestFoo(t *testing.T) {
+ go func() {
+ t.Fatal("oops") // error: (*T).Fatal called from non-test goroutine
+ }()
+}
+
+
+**Enabled by default.**
+
+## **tests**
check for common mistaken usages of tests and examples
@@ -257,18 +408,18 @@
Please see the documentation for package testing in golang.org/pkg/testing
for the conventions that are enforced for Tests, Benchmarks, and Examples.
-Default value: `true`.
+**Enabled by default.**
-### **unmarshal**
+## **unmarshal**
report passing non-pointer or non-interface values to unmarshal
The unmarshal analysis reports calls to functions such as json.Unmarshal
in which the argument type is not a pointer or an interface.
-Default value: `true`.
+**Enabled by default.**
-### **unreachable**
+## **unreachable**
check for unreachable code
@@ -276,9 +427,9 @@
because they are preceded by an return statement, a call to panic, an
infinite loop, or similar constructs.
-Default value: `true`.
+**Enabled by default.**
-### **unsafeptr**
+## **unsafeptr**
check for invalid conversions of uintptr to unsafe.Pointer
@@ -288,201 +439,9 @@
word in memory that holds a pointer value, because that word will be
invisible to stack copying and to the garbage collector.
-Default value: `true`.
+**Enabled by default.**
-### **unusedresult**
-
-check for unused results of calls to some functions
-
-Some functions like fmt.Errorf return a result and have no side effects,
-so it is always a mistake to discard the result. This analyzer reports
-calls to certain functions in which the result of the call is ignored.
-
-The set of functions may be controlled using flags.
-
-Default value: `true`.
-
-## gopls suite
-
-Below is the list of analyzers that are used by `gopls`.
-
-### **deepequalerrors**
-
-check for calls of reflect.DeepEqual on error values
-
-The deepequalerrors checker looks for calls of the form:
-
-```go
- reflect.DeepEqual(err1, err2)
-```
-
-where err1 and err2 are errors. Using reflect.DeepEqual to compare
-errors is discouraged.
-
-Default value: `true`.
-
-### **fieldalignment**
-
-This analyzer find structs that can be rearranged to take less memory, and provides
-a suggested edit with the optimal order.
-
-Default value: `false`.
-
-### **fillreturns**
-
-suggested fixes for "wrong number of return values (want %d, got %d)"
-
-This checker provides suggested fixes for type errors of the
-type "wrong number of return values (want %d, got %d)". For example:
-```go
-func m() (int, string, *bool, error) {
- return
-}
-```
-will turn into
-```go
-func m() (int, string, *bool, error) {
- return 0, "", nil, nil
-}
-```
-
-This functionality is similar to [goreturns](https://github.com/sqs/goreturns).
-
-Default value: `false`.
-
-### **nonewvars**
-
-suggested fixes for "no new vars on left side of :="
-
-This checker provides suggested fixes for type errors of the
-type "no new vars on left side of :=". For example:
-```go
-z := 1
-z := 2
-```
-will turn into
-```go
-z := 1
-z = 2
-```
-
-Default value: `false`.
-
-### **noresultvalues**
-
-suggested fixes for "no result values expected"
-
-This checker provides suggested fixes for type errors of the
-type "no result values expected". For example:
-```go
-func z() { return nil }
-```
-will turn into
-```go
-func z() { return }
-```
-
-Default value: `true`.
-
-### **simplifycompositelit**
-
-check for composite literal simplifications
-
-An array, slice, or map composite literal of the form:
-```go
-[]T{T{}, T{}}
-```
-will be simplified to:
-```go
-[]T{{}, {}}
-```
-
-This is one of the simplifications that "gofmt -s" applies.
-
-Default value: `true`.
-
-### **simplifyrange**
-
-check for range statement simplifications
-
-A range of the form:
-```go
-for x, _ = range v {...}
-```
-will be simplified to:
-```go
-for x = range v {...}
-```
-
-A range of the form:
-```go
-for _ = range v {...}
-```
-will be simplified to:
-```go
-for range v {...}
-```
-
-This is one of the simplifications that "gofmt -s" applies.
-
-Default value: `true`.
-
-### **simplifyslice**
-
-check for slice simplifications
-
-A slice expression of the form:
-```go
-s[a:len(s)]
-```
-will be simplified to:
-```go
-s[a:]
-```
-
-This is one of the simplifications that "gofmt -s" applies.
-
-Default value: `true`.
-
-### **sortslice**
-
-check the argument type of sort.Slice
-
-sort.Slice requires an argument of a slice type. Check that
-the interface{} value passed to sort.Slice is actually a slice.
-
-Default value: `true`.
-
-### **testinggoroutine**
-
-report calls to (*testing.T).Fatal from goroutines started by a test.
-
-Functions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and
-Skip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.
-This checker detects calls to these functions that occur within a goroutine
-started by the test. For example:
-
-```go
-func TestFoo(t *testing.T) {
- go func() {
- t.Fatal("oops") // error: (*T).Fatal called from non-test goroutine
- }()
-}
-```
-
-Default value: `true`.
-
-### **undeclaredname**
-
-suggested fixes for "undeclared name: <>"
-
-This checker provides suggested fixes for type errors of the
-type `undeclared name: <>`. It will insert a new statement:
-`<> := `.
-
-Default value: `false`.
-
-### **unusedparams**
+## **unusedparams**
check for unused parameters of functions
@@ -495,4 +454,87 @@
- functions in test files
- functions with empty bodies or those with just a return stmt
-Default value: `false`.
+**Disabled by default. Enable it by setting `"analyses": {"unusedparams": true}`.**
+
+## **unusedresult**
+
+check for unused results of calls to some functions
+
+Some functions like fmt.Errorf return a result and have no side effects,
+so it is always a mistake to discard the result. This analyzer reports
+calls to certain functions in which the result of the call is ignored.
+
+The set of functions may be controlled using flags.
+
+**Enabled by default.**
+
+## **fillreturns**
+
+suggested fixes for "wrong number of return values (want %d, got %d)"
+
+This checker provides suggested fixes for type errors of the
+type "wrong number of return values (want %d, got %d)". For example:
+ func m() (int, string, *bool, error) {
+ return
+ }
+will turn into
+ func m() (int, string, *bool, error) {
+ return 0, "", nil, nil
+ }
+
+This functionality is similar to https://github.com/sqs/goreturns.
+
+
+**Enabled by default.**
+
+## **nonewvars**
+
+suggested fixes for "no new vars on left side of :="
+
+This checker provides suggested fixes for type errors of the
+type "no new vars on left side of :=". For example:
+ z := 1
+ z := 2
+will turn into
+ z := 1
+ z = 2
+
+
+**Enabled by default.**
+
+## **noresultvalues**
+
+suggested fixes for "no result values expected"
+
+This checker provides suggested fixes for type errors of the
+type "no result values expected". For example:
+ func z() { return nil }
+will turn into
+ func z() { return }
+
+
+**Enabled by default.**
+
+## **undeclaredname**
+
+suggested fixes for "undeclared name: <>"
+
+This checker provides suggested fixes for type errors of the
+type "undeclared name: <>". It will insert a new statement:
+"<> := ".
+
+**Enabled by default.**
+
+## **fillstruct**
+
+note incomplete struct initializations
+
+This analyzer provides diagnostics for any struct literals that do not have
+any fields initialized. Because the suggested fix for this analysis is
+expensive to compute, callers should compute it separately, using the
+SuggestedFix function below.
+
+
+**Enabled by default.**
+
+<!-- END Analyzers: DO NOT MANUALLY EDIT THIS SECTION -->
diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go
index 3be68ce..1eff8b9 100644
--- a/gopls/doc/generate.go
+++ b/gopls/doc/generate.go
@@ -14,13 +14,17 @@
"go/format"
"go/token"
"go/types"
+ "io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
+ "sort"
+ "strconv"
"strings"
"time"
+ "unicode"
"github.com/sanity-io/litter"
"golang.org/x/tools/go/ast/astutil"
@@ -51,6 +55,9 @@
if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/commands.md"), api, write, rewriteCommands); !ok || err != nil {
return ok, err
}
+ if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/analyzers.md"), api, write, rewriteAnalyzers); !ok || err != nil {
+ return ok, err
+ }
return true, nil
}
@@ -71,18 +78,6 @@
Options: map[string][]*source.OptionJSON{},
}
defaults := source.DefaultOptions()
- for _, cat := range []reflect.Value{
- reflect.ValueOf(defaults.DebuggingOptions),
- reflect.ValueOf(defaults.UserOptions),
- reflect.ValueOf(defaults.ExperimentalOptions),
- } {
- opts, err := loadOptions(cat, pkg)
- if err != nil {
- return nil, err
- }
- catName := strings.TrimSuffix(cat.Type().Name(), "Options")
- api.Options[catName] = opts
- }
api.Commands, err = loadCommands(pkg)
if err != nil {
@@ -94,16 +89,65 @@
for _, c := range api.Commands {
c.Command = source.CommandPrefix + c.Command
}
+ for _, m := range []map[string]source.Analyzer{
+ defaults.DefaultAnalyzers,
+ defaults.TypeErrorAnalyzers,
+ defaults.ConvenienceAnalyzers,
+ // Don't yet add staticcheck analyzers.
+ } {
+ api.Analyzers = append(api.Analyzers, loadAnalyzers(m)...)
+ }
+ for _, category := range []reflect.Value{
+ reflect.ValueOf(defaults.UserOptions),
+ } {
+ // Find the type information and ast.File corresponding to the category.
+ optsType := pkg.Types.Scope().Lookup(category.Type().Name())
+ if optsType == nil {
+ return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
+ }
+ opts, err := loadOptions(category, optsType, pkg, "")
+ if err != nil {
+ return nil, err
+ }
+ catName := strings.TrimSuffix(category.Type().Name(), "Options")
+ api.Options[catName] = opts
+
+ // Hardcode the expected values for the analyses and code lenses
+ // settings, since their keys are not enums.
+ for _, opt := range opts {
+ switch opt.Name {
+ case "analyses":
+ for _, a := range api.Analyzers {
+ opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, source.EnumKey{
+ Name: fmt.Sprintf("%q", a.Name),
+ Doc: a.Doc,
+ Default: strconv.FormatBool(a.Default),
+ })
+ }
+ case "codelenses":
+ // Hack: Lenses don't set default values, and we don't want to
+ // pass in the list of expected lenses to loadOptions. Instead,
+ // format the defaults using reflection here. The hackiest part
+ // is reversing lowercasing of the field name.
+ reflectField := category.FieldByName(upperFirst(opt.Name))
+ for _, l := range api.Lenses {
+ def, err := formatDefaultFromEnumBoolMap(reflectField, l.Lens)
+ if err != nil {
+ return nil, err
+ }
+ opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, source.EnumKey{
+ Name: fmt.Sprintf("%q", l.Lens),
+ Doc: l.Doc,
+ Default: def,
+ })
+ }
+ }
+ }
+ }
return api, nil
}
-func loadOptions(category reflect.Value, pkg *packages.Package) ([]*source.OptionJSON, error) {
- // Find the type information and ast.File corresponding to the category.
- optsType := pkg.Types.Scope().Lookup(category.Type().Name())
- if optsType == nil {
- return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
- }
-
+func loadOptions(category reflect.Value, optsType types.Object, pkg *packages.Package, hierarchy string) ([]*source.OptionJSON, error) {
file, err := fileForPos(pkg, optsType.Pos())
if err != nil {
return nil, err
@@ -119,6 +163,21 @@
for i := 0; i < optsStruct.NumFields(); i++ {
// The types field gives us the type.
typesField := optsStruct.Field(i)
+
+ // If the field name ends with "Options", assume it is a struct with
+ // additional options and process it recursively.
+ if h := strings.TrimSuffix(typesField.Name(), "Options"); h != typesField.Name() {
+ // Keep track of the parent structs.
+ if hierarchy != "" {
+ h = hierarchy + "." + h
+ }
+ options, err := loadOptions(category, typesField, pkg, strings.ToLower(h))
+ if err != nil {
+ return nil, err
+ }
+ opts = append(opts, options...)
+ continue
+ }
path, _ := astutil.PathEnclosingInterval(file, typesField.Pos(), typesField.Pos())
if len(path) < 2 {
return nil, fmt.Errorf("could not find AST node for field %v", typesField)
@@ -135,40 +194,48 @@
return nil, fmt.Errorf("could not find reflect field for %v", typesField.Name())
}
- // Format the default value. VSCode exposes settings as JSON, so showing them as JSON is reasonable.
- def := reflectField.Interface()
- // Durations marshal as nanoseconds, but we want the stringy versions, e.g. "100ms".
- if t, ok := def.(time.Duration); ok {
- def = t.String()
- }
- defBytes, err := json.Marshal(def)
+ def, err := formatDefault(reflectField)
if err != nil {
return nil, err
}
- // Nil values format as "null" so print them as hardcoded empty values.
- switch reflectField.Type().Kind() {
- case reflect.Map:
- if reflectField.IsNil() {
- defBytes = []byte("{}")
- }
- case reflect.Slice:
- if reflectField.IsNil() {
- defBytes = []byte("[]")
- }
- }
-
typ := typesField.Type().String()
if _, ok := enums[typesField.Type()]; ok {
typ = "enum"
}
+ name := lowerFirst(typesField.Name())
+
+ var enumKeys source.EnumKeys
+ if m, ok := typesField.Type().(*types.Map); ok {
+ e, ok := enums[m.Key()]
+ if ok {
+ typ = strings.Replace(typ, m.Key().String(), m.Key().Underlying().String(), 1)
+ }
+ keys, err := collectEnumKeys(name, m, reflectField, e)
+ if err != nil {
+ return nil, err
+ }
+ if keys != nil {
+ enumKeys = *keys
+ }
+ }
+
+ // Get the status of the field by checking its struct tags.
+ reflectStructField, ok := category.Type().FieldByName(typesField.Name())
+ if !ok {
+ return nil, fmt.Errorf("no struct field for %s", typesField.Name())
+ }
+ status := reflectStructField.Tag.Get("status")
opts = append(opts, &source.OptionJSON{
- Name: lowerFirst(typesField.Name()),
+ Name: name,
Type: typ,
Doc: lowerFirst(astField.Doc.Text()),
- Default: string(defBytes),
+ Default: def,
+ EnumKeys: enumKeys,
EnumValues: enums[typesField.Type()],
+ Status: status,
+ Hierarchy: hierarchy,
})
}
return opts, nil
@@ -199,6 +266,90 @@
return enums, nil
}
+func collectEnumKeys(name string, m *types.Map, reflectField reflect.Value, enumValues []source.EnumValue) (*source.EnumKeys, error) {
+ // Make sure the value type gets set for analyses and codelenses
+ // too.
+ if len(enumValues) == 0 && !hardcodedEnumKeys(name) {
+ return nil, nil
+ }
+ keys := &source.EnumKeys{
+ ValueType: m.Elem().String(),
+ }
+ // We can get default values for enum -> bool maps.
+ var isEnumBoolMap bool
+ if basic, ok := m.Elem().(*types.Basic); ok && basic.Kind() == types.Bool {
+ isEnumBoolMap = true
+ }
+ for _, v := range enumValues {
+ var def string
+ if isEnumBoolMap {
+ var err error
+ def, err = formatDefaultFromEnumBoolMap(reflectField, v.Value)
+ if err != nil {
+ return nil, err
+ }
+ }
+ keys.Keys = append(keys.Keys, source.EnumKey{
+ Name: v.Value,
+ Doc: v.Doc,
+ Default: def,
+ })
+ }
+ return keys, nil
+}
+
+func formatDefaultFromEnumBoolMap(reflectMap reflect.Value, enumKey string) (string, error) {
+ if reflectMap.Kind() != reflect.Map {
+ return "", nil
+ }
+ name := enumKey
+ if unquoted, err := strconv.Unquote(name); err == nil {
+ name = unquoted
+ }
+ for _, e := range reflectMap.MapKeys() {
+ if e.String() == name {
+ value := reflectMap.MapIndex(e)
+ if value.Type().Kind() == reflect.Bool {
+ return formatDefault(value)
+ }
+ }
+ }
+ // Assume that if the value isn't mentioned in the map, it defaults to
+ // the default value, false.
+ return formatDefault(reflect.ValueOf(false))
+}
+
+// formatDefault formats the default value into a JSON-like string.
+// VS Code exposes settings as JSON, so showing them as JSON is reasonable.
+// TODO(rstambler): Reconsider this approach, as the VS Code Go generator now
+// marshals to JSON.
+func formatDefault(reflectField reflect.Value) (string, error) {
+ def := reflectField.Interface()
+
+ // Durations marshal as nanoseconds, but we want the stringy versions,
+ // e.g. "100ms".
+ if t, ok := def.(time.Duration); ok {
+ def = t.String()
+ }
+ defBytes, err := json.Marshal(def)
+ if err != nil {
+ return "", err
+ }
+
+ // Nil values format as "null" so print them as hardcoded empty values.
+ switch reflectField.Type().Kind() {
+ case reflect.Map:
+ if reflectField.IsNil() {
+ defBytes = []byte("{}")
+ }
+ case reflect.Slice:
+ if reflectField.IsNil() {
+ defBytes = []byte("[]")
+ }
+ }
+ return string(defBytes), err
+}
+
// valueDoc transforms a docstring documenting an constant identifier to a
// docstring documenting its value.
//
@@ -311,6 +462,24 @@
return lenses
}
+func loadAnalyzers(m map[string]source.Analyzer) []*source.AnalyzerJSON {
+ var sorted []string
+ for _, a := range m {
+ sorted = append(sorted, a.Analyzer.Name)
+ }
+ sort.Strings(sorted)
+ var json []*source.AnalyzerJSON
+ for _, name := range sorted {
+ a := m[name]
+ json = append(json, &source.AnalyzerJSON{
+ Name: a.Analyzer.Name,
+ Doc: a.Analyzer.Doc,
+ Default: a.Enabled,
+ })
+ }
+ return json
+}
+
func lowerFirst(x string) string {
if x == "" {
return x
@@ -318,6 +487,13 @@
return strings.ToLower(x[:1]) + x[1:]
}
+func upperFirst(x string) string {
+ if x == "" {
+ return x
+ }
+ return strings.ToUpper(x[:1]) + x[1:]
+}
+
func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) {
fset := pkg.Fset
for _, f := range pkg.Syntax {
@@ -350,7 +526,7 @@
return true, nil
}
-func rewriteAPI(input []byte, api *source.APIJSON) ([]byte, error) {
+func rewriteAPI(_ []byte, api *source.APIJSON) ([]byte, error) {
buf := bytes.NewBuffer(nil)
apiStr := litter.Options{
HomePackage: "source",
@@ -360,7 +536,9 @@
apiStr = strings.ReplaceAll(apiStr, ": []*OptionJSON", ":")
apiStr = strings.ReplaceAll(apiStr, "&CommandJSON", "")
apiStr = strings.ReplaceAll(apiStr, "&LensJSON", "")
+ apiStr = strings.ReplaceAll(apiStr, "&AnalyzerJSON", "")
apiStr = strings.ReplaceAll(apiStr, " EnumValue{", "{")
+ apiStr = strings.ReplaceAll(apiStr, " EnumKey{", "{")
apiBytes, err := format.Source([]byte(apiStr))
if err != nil {
return nil, err
@@ -371,25 +549,39 @@
var parBreakRE = regexp.MustCompile("\n{2,}")
+type optionsGroup struct {
+ title string
+ final string
+ level int
+ options []*source.OptionJSON
+}
+
func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) {
result := doc
for category, opts := range api.Options {
+ groups := collectGroups(opts)
+
+ // First, print a table of contents.
section := bytes.NewBuffer(nil)
- for _, opt := range opts {
- var enumValues strings.Builder
- if len(opt.EnumValues) > 0 {
- enumValues.WriteString("Must be one of:\n\n")
- for _, val := range opt.EnumValues {
- if val.Doc != "" {
- // Don't break the list item by starting a new paragraph.
- unbroken := parBreakRE.ReplaceAllString(val.Doc, "\\\n")
- fmt.Fprintf(&enumValues, " * %s\n", unbroken)
- } else {
- fmt.Fprintf(&enumValues, " * `%s`\n", val.Value)
- }
- }
+ fmt.Fprintln(section, "")
+ for _, h := range groups {
+ writeBullet(section, h.final, h.level)
+ }
+ fmt.Fprintln(section, "")
+
+ // Currently, the settings document has a title and a subtitle, so
+ // start at level 3 for a header beginning with "###".
+ baseLevel := 3
+ for _, h := range groups {
+ level := baseLevel + h.level
+ writeTitle(section, h.final, level)
+ for _, opt := range h.options {
+ header := strMultiply("#", level+1)
+ fmt.Fprintf(section, "%s **%v** *%v*\n\n", header, opt.Name, opt.Type)
+ writeStatus(section, opt.Status)
+ enumValues := collectEnums(opt)
+ fmt.Fprintf(section, "%v%v\nDefault: `%v`.\n\n", opt.Doc, enumValues, opt.Default)
}
- fmt.Fprintf(section, "### **%v** *%v*\n%v%v\n\nDefault: `%v`.\n", opt.Name, opt.Type, opt.Doc, enumValues.String(), opt.Default)
}
var err error
result, err = replaceSection(result, category, section.Bytes())
@@ -400,11 +592,144 @@
section := bytes.NewBuffer(nil)
for _, lens := range api.Lenses {
- fmt.Fprintf(section, "### **%v**\nIdentifier: `%v`\n\n%v\n\n", lens.Title, lens.Lens, lens.Doc)
+ fmt.Fprintf(section, "### **%v**\n\nIdentifier: `%v`\n\n%v\n", lens.Title, lens.Lens, lens.Doc)
}
return replaceSection(result, "Lenses", section.Bytes())
}
+func collectGroups(opts []*source.OptionJSON) []optionsGroup {
+ optsByHierarchy := map[string][]*source.OptionJSON{}
+ for _, opt := range opts {
+ optsByHierarchy[opt.Hierarchy] = append(optsByHierarchy[opt.Hierarchy], opt)
+ }
+
+ // As a hack, assume that uncategorized items are less important to
+ // users and force the empty string to the end of the list.
+ var containsEmpty bool
+ var sorted []string
+ for h := range optsByHierarchy {
+ if h == "" {
+ containsEmpty = true
+ continue
+ }
+ sorted = append(sorted, h)
+ }
+ sort.Strings(sorted)
+ if containsEmpty {
+ sorted = append(sorted, "")
+ }
+ var groups []optionsGroup
+ baseLevel := 0
+ for _, h := range sorted {
+ split := strings.SplitAfter(h, ".")
+ last := split[len(split)-1]
+ // Hack to capitalize all of UI.
+ if last == "ui" {
+ last = "UI"
+ }
+ // A hierarchy may look like "ui.formatting". If "ui" has no
+ // options of its own, it may not be added to the map, but it
+ // still needs a heading.
+ components := strings.Split(h, ".")
+ for i := 1; i < len(components); i++ {
+ parent := strings.Join(components[0:i], ".")
+ if _, ok := optsByHierarchy[parent]; !ok {
+ groups = append(groups, optionsGroup{
+ title: parent,
+ final: last,
+ level: baseLevel + i,
+ })
+ }
+ }
+ groups = append(groups, optionsGroup{
+ title: h,
+ final: last,
+ level: baseLevel + strings.Count(h, "."),
+ options: optsByHierarchy[h],
+ })
+ }
+ return groups
+}
+
+func collectEnums(opt *source.OptionJSON) string {
+ var b strings.Builder
+ write := func(name, doc string, index, len int) {
+ if doc != "" {
+ unbroken := parBreakRE.ReplaceAllString(doc, "\\\n")
+ fmt.Fprintf(&b, "* %s", unbroken)
+ } else {
+ fmt.Fprintf(&b, "* `%s`", name)
+ }
+ if index < len-1 {
+ fmt.Fprint(&b, "\n")
+ }
+ }
+ if len(opt.EnumValues) > 0 && opt.Type == "enum" {
+ b.WriteString("\nMust be one of:\n\n")
+ for i, val := range opt.EnumValues {
+ write(val.Value, val.Doc, i, len(opt.EnumValues))
+ }
+ } else if len(opt.EnumKeys.Keys) > 0 && shouldShowEnumKeysInSettings(opt.Name) {
+ b.WriteString("\nCan contain any of:\n\n")
+ for i, val := range opt.EnumKeys.Keys {
+ write(val.Name, val.Doc, i, len(opt.EnumKeys.Keys))
+ }
+ }
+ return b.String()
+}
+
+func shouldShowEnumKeysInSettings(name string) bool {
+ // Both of these fields have too many possible options to print.
+ return !hardcodedEnumKeys(name)
+}
+
+func hardcodedEnumKeys(name string) bool {
+ return name == "analyses" || name == "codelenses"
+}
+
+func writeBullet(w io.Writer, title string, level int) {
+ if title == "" {
+ return
+ }
+ // Capitalize the first letter of each title.
+ prefix := strMultiply(" ", level)
+ fmt.Fprintf(w, "%s* [%s](#%s)\n", prefix, capitalize(title), strings.ToLower(title))
+}
+
+func writeTitle(w io.Writer, title string, level int) {
+ if title == "" {
+ return
+ }
+ // Capitalize the first letter of each title.
+ fmt.Fprintf(w, "%s %s\n\n", strMultiply("#", level), capitalize(title))
+}
+
+func writeStatus(section io.Writer, status string) {
+ switch status {
+ case "":
+ case "advanced":
+ fmt.Fprint(section, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n")
+ case "debug":
+ fmt.Fprint(section, "**This setting is for debugging purposes only.**\n\n")
+ case "experimental":
+ fmt.Fprint(section, "**This setting is experimental and may be deleted.**\n\n")
+ default:
+ fmt.Fprintf(section, "**Status: %s.**\n\n", status)
+ }
+}
+
+func capitalize(s string) string {
+ return string(unicode.ToUpper(rune(s[0]))) + s[1:]
+}
+
+func strMultiply(str string, count int) string {
+ var result string
+ for i := 0; i < count; i++ {
+ result += string(str)
+ }
+ return result
+}
+
func rewriteCommands(doc []byte, api *source.APIJSON) ([]byte, error) {
section := bytes.NewBuffer(nil)
for _, command := range api.Commands {
@@ -413,6 +738,21 @@
return replaceSection(doc, "Commands", section.Bytes())
}
+func rewriteAnalyzers(doc []byte, api *source.APIJSON) ([]byte, error) {
+ section := bytes.NewBuffer(nil)
+ for _, analyzer := range api.Analyzers {
+ fmt.Fprintf(section, "## **%v**\n\n", analyzer.Name)
+ fmt.Fprintf(section, "%s\n\n", analyzer.Doc)
+ switch analyzer.Default {
+ case true:
+ fmt.Fprintf(section, "**Enabled by default.**\n\n")
+ case false:
+ fmt.Fprintf(section, "**Disabled by default. Enable it by setting `\"analyses\": {\"%s\": true}`.**\n\n", analyzer.Name)
+ }
+ }
+ return replaceSection(doc, "Analyzers", section.Bytes())
+}
+
func replaceSection(doc []byte, sectionName string, replacement []byte) ([]byte, error) {
re := regexp.MustCompile(fmt.Sprintf(`(?s)<!-- BEGIN %v.* -->\n(.*?)<!-- END %v.* -->`, sectionName, sectionName))
idx := re.FindSubmatchIndex(doc)
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index d42cb13..cf465ce 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -2,9 +2,13 @@
<!--TODO: Generate this file from the documentation in golang/org/x/tools/internal/lsp/source/options.go.-->
-This document describes the 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. These settings can also be configured differently per workspace folder.
+This document describes the 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.
+These settings can also be configured differently per workspace folder.
-In VSCode, this would be a section in your `settings.json` file that might look like this:
+In VSCode, this would be a section in your `settings.json` file that might look
+like this:
```json5
"gopls": {
@@ -17,165 +21,40 @@
Below is the list of settings that are officially supported for `gopls`.
+Any settings that are experimental or for debugging purposes are marked as
+such.
+
To enable all experimental features, use **allExperiments: `true`**. You will
still be able to independently override specific experimental features.
<!-- BEGIN User: DO NOT MANUALLY EDIT THIS SECTION -->
-### **buildFlags** *[]string*
+
+* [Build](#build)
+* [Formatting](#formatting)
+* [UI](#ui)
+ * [Completion](#completion)
+ * [Diagnostic](#diagnostic)
+ * [Documentation](#documentation)
+ * [Navigation](#navigation)
+
+### Build
+
+#### **buildFlags** *[]string*
+
buildFlags is the set of flags passed on to the build system when invoked.
It is applied to queries like `go list`, which is used when discovering files.
The most common use is to set `-tags`.
-
Default: `[]`.
-### **env** *map[string]string*
+
+#### **env** *map[string]string*
+
env adds environment variables to external commands run by `gopls`, most notably `go list`.
-
Default: `{}`.
-### **hoverKind** *enum*
-hoverKind controls the information that appears in the hover text.
-SingleLine and Structured are intended for use only by authors of editor plugins.
-Must be one of:
- * `"FullDocumentation"`
- * `"NoDocumentation"`
- * `"SingleLine"`
- * `"Structured"` is an experimental setting that returns a structured hover format.
-This format separates the signature from the documentation, so that the client
-can do more manipulation of these fields.\
-This should only be used by clients that support this behavior.
+#### **directoryFilters** *[]string*
- * `"SynopsisDocumentation"`
-
-
-Default: `"FullDocumentation"`.
-### **usePlaceholders** *bool*
-placeholders enables placeholders for function parameters or struct fields in completion responses.
-
-
-Default: `false`.
-### **linkTarget** *string*
-linkTarget controls where documentation links go.
-It might be one of:
-
-* `"godoc.org"`
-* `"pkg.go.dev"`
-
-If company chooses to use its own `godoc.org`, its address can be used as well.
-
-
-Default: `"pkg.go.dev"`.
-### **local** *string*
-local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.
-It should be the prefix of the import path whose imports should be grouped separately.
-
-
-Default: `""`.
-### **gofumpt** *bool*
-gofumpt indicates if we should run gofumpt formatting.
-
-
-Default: `false`.
-### **analyses** *map[string]bool*
-analyses specify analyses that the user would like to enable or disable.
-A map of the names of analysis passes that should be enabled/disabled.
-A full list of analyzers that gopls uses can be found [here](analyzers.md)
-
-Example Usage:
-```json5
-...
-"analyses": {
- "unreachable": false, // Disable the unreachable analyzer.
- "unusedparams": true // Enable the unusedparams analyzer.
-}
-...
-```
-
-
-Default: `{}`.
-### **codelenses** *map[string]bool*
-codelenses overrides the enabled/disabled state of code lenses. See the "Code Lenses"
-section of settings.md for the list of supported lenses.
-
-Example Usage:
-```json5
-"gopls": {
-...
- "codelens": {
- "generate": false, // Don't show the `go generate` lens.
- "gc_details": true // Show a code lens toggling the display of gc's choices.
- }
-...
-}
-```
-
-
-Default: `{"gc_details":false,"generate":true,"regenerate_cgo":true,"tidy":true,"upgrade_dependency":true,"vendor":true}`.
-### **linksInHover** *bool*
-linksInHover toggles the presence of links to documentation in hover.
-
-
-Default: `true`.
-### **importShortcut** *enum*
-importShortcut specifies whether import statements should link to
-documentation or go to definitions.
-Must be one of:
-
- * `"Both"`
- * `"Definition"`
- * `"Link"`
-
-
-Default: `"Both"`.
-### **matcher** *enum*
-matcher sets the algorithm that is used when calculating completion candidates.
-Must be one of:
-
- * `"CaseInsensitive"`
- * `"CaseSensitive"`
- * `"Fuzzy"`
-
-
-Default: `"Fuzzy"`.
-### **symbolMatcher** *enum*
-symbolMatcher sets the algorithm that is used when finding workspace symbols.
-Must be one of:
-
- * `"CaseInsensitive"`
- * `"CaseSensitive"`
- * `"Fuzzy"`
-
-
-Default: `"Fuzzy"`.
-### **symbolStyle** *enum*
-symbolStyle controls how symbols are qualified in symbol responses.
-
-Example Usage:
-```json5
-"gopls": {
-...
- "symbolStyle": "dynamic",
-...
-}
-```
-Must be one of:
-
- * `"Dynamic"` uses whichever qualifier results in the highest scoring
-match for the given symbol query. Here a "qualifier" is any "/" or "."
-delimited suffix of the fully qualified symbol. i.e. "to/pkg.Foo.Field" or
-just "Foo.Field".
-
- * `"Full"` is fully qualified symbols, i.e.
-"path/to/pkg.Foo.Field".
-
- * `"Package"` is package qualified symbols i.e.
-"pkg.Foo.Field".
-
-
-
-Default: `"Dynamic"`.
-### **directoryFilters** *[]string*
directoryFilters can be used to exclude unwanted directories from the
workspace. By default, all directories are included. Filters are an
operator, `+` to include and `-` to exclude, followed by a path prefix
@@ -188,37 +67,12 @@
Include only project_a: `-` (exclude everything), `+project_a`
Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`
-
Default: `[]`.
-<!-- END User: DO NOT MANUALLY EDIT THIS SECTION -->
-## Experimental
+#### **expandWorkspaceToModule** *bool*
-The below settings are considered experimental. They may be deprecated or changed in the future. They are typically used to test experimental opt-in features or to disable features.
+**This setting is experimental and may be deleted.**
-<!-- BEGIN Experimental: DO NOT MANUALLY EDIT THIS SECTION -->
-### **annotations** *map[string]bool*
-annotations suppress various kinds of optimization diagnostics
-that would be reported by the gc_details command.
- * noNilcheck suppresses display of nilchecks.
- * noEscape suppresses escape choices.
- * noInline suppresses inlining choices.
- * noBounds suppresses bounds checking diagnostics.
-
-
-Default: `{}`.
-### **staticcheck** *bool*
-staticcheck enables additional analyses from staticcheck.io.
-
-
-Default: `false`.
-### **semanticTokens** *bool*
-semanticTokens controls whether the LSP server will send
-semantic tokens to the client.
-
-
-Default: `false`.
-### **expandWorkspaceToModule** *bool*
expandWorkspaceToModule instructs `gopls` to adjust the scope of the
workspace to find the best available module root. `gopls` first looks for
a go.mod file in any parent directory of the workspace folder, expanding
@@ -226,25 +80,21 @@
found, gopls will check if there is exactly one child directory containing
a go.mod file, narrowing the scope to that directory if it exists.
-
Default: `true`.
-### **experimentalWorkspaceModule** *bool*
+
+#### **experimentalWorkspaceModule** *bool*
+
+**This setting is experimental and may be deleted.**
+
experimentalWorkspaceModule opts a user into the experimental support
for multi-module workspaces.
-
Default: `false`.
-### **experimentalDiagnosticsDelay** *time.Duration*
-experimentalDiagnosticsDelay controls the amount of time that gopls waits
-after the most recent file modification before computing deep diagnostics.
-Simple diagnostics (parsing and type-checking) are always run immediately
-on recently modified packages.
-This option must be set to a valid duration string, for example `"250ms"`.
+#### **experimentalPackageCacheKey** *bool*
+**This setting is experimental and may be deleted.**
-Default: `"250ms"`.
-### **experimentalPackageCacheKey** *bool*
experimentalPackageCacheKey controls whether to use a coarser cache key
for package type information to increase cache hits. This setting removes
the user's environment, build flags, and working directory from the cache
@@ -253,89 +103,327 @@
by an experiment because caching behavior is subtle and difficult to
comprehensively test.
-
Default: `true`.
-### **allowModfileModifications** *bool*
+
+#### **allowModfileModifications** *bool*
+
+**This setting is experimental and may be deleted.**
+
allowModfileModifications disables -mod=readonly, allowing imports from
out-of-scope modules. This option will eventually be removed.
-
Default: `false`.
-### **allowImplicitNetworkAccess** *bool*
+
+#### **allowImplicitNetworkAccess** *bool*
+
+**This setting is experimental and may be deleted.**
+
allowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module
downloads rather than requiring user action. This option will eventually
be removed.
+Default: `false`.
+
+### Formatting
+
+#### **local** *string*
+
+local is the equivalent of the `goimports -local` flag, which puts
+imports beginning with this string after third-party packages. It should
+be the prefix of the import path whose imports should be grouped
+separately.
+
+Default: `""`.
+
+#### **gofumpt** *bool*
+
+gofumpt indicates if we should run gofumpt formatting.
Default: `false`.
-<!-- END Experimental: DO NOT MANUALLY EDIT THIS SECTION -->
-## Debugging
+### UI
-The below settings are for use in debugging `gopls`. Like the experimental options, they may be deprecated or changed in the future.
+#### **codelenses** *map[string]bool*
-<!-- BEGIN Debugging: DO NOT MANUALLY EDIT THIS SECTION -->
-### **verboseOutput** *bool*
-verboseOutput enables additional debug logging.
+codelenses overrides the enabled/disabled state of code lenses. See the
+"Code Lenses" section of the
+[Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md)
+for the list of supported lenses.
+Example Usage:
+
+```json5
+"gopls": {
+...
+ "codelens": {
+ "generate": false, // Don't show the `go generate` lens.
+ "gc_details": true // Show a code lens toggling the display of gc's choices.
+ }
+...
+}
+```
+
+Default: `{"gc_details":false,"generate":true,"regenerate_cgo":true,"tidy":true,"upgrade_dependency":true,"vendor":true}`.
+
+#### **semanticTokens** *bool*
+
+**This setting is experimental and may be deleted.**
+
+semanticTokens controls whether the LSP server will send
+semantic tokens to the client.
Default: `false`.
-### **completionBudget** *time.Duration*
+
+#### Completion
+
+##### **usePlaceholders** *bool*
+
+placeholders enables placeholders for function parameters or struct
+fields in completion responses.
+
+Default: `false`.
+
+##### **completionBudget** *time.Duration*
+
+**This setting is for debugging purposes only.**
+
completionBudget is the soft latency goal for completion requests. Most
requests finish in a couple milliseconds, but in some cases deep
completions can take much longer. As we use up our budget we
dynamically reduce the search scope to ensure we return timely
results. Zero means unlimited.
-
Default: `"100ms"`.
-<!-- END Debugging: DO NOT MANUALLY EDIT THIS SECTION -->
+
+##### **matcher** *enum*
+
+**This is an advanced setting and should not be configured by most `gopls` users.**
+
+matcher sets the algorithm that is used when calculating completion
+candidates.
+
+Must be one of:
+
+* `"CaseInsensitive"`
+* `"CaseSensitive"`
+* `"Fuzzy"`
+Default: `"Fuzzy"`.
+
+#### Diagnostic
+
+##### **analyses** *map[string]bool*
+
+analyses specify analyses that the user would like to enable or disable.
+A map of the names of analysis passes that should be enabled/disabled.
+A full list of analyzers that gopls uses can be found
+[here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).
+
+Example Usage:
+
+```json5
+...
+"analyses": {
+ "unreachable": false, // Disable the unreachable analyzer.
+ "unusedparams": true // Enable the unusedparams analyzer.
+}
+...
+```
+
+Default: `{}`.
+
+##### **staticcheck** *bool*
+
+**This setting is experimental and may be deleted.**
+
+staticcheck enables additional analyses from staticcheck.io.
+
+Default: `false`.
+
+##### **annotations** *map[string]bool*
+
+**This setting is experimental and may be deleted.**
+
+annotations specifies the various kinds of optimization diagnostics
+that should be reported by the gc_details command.
+
+Can contain any of:
+
+* `"bounds"` controls bounds checking diagnostics.
+
+* `"escape"` controls diagnostics about escape choices.
+
+* `"inline"` controls diagnostics about inlining choices.
+
+* `"nil"` controls nil checks.
+
+Default: `{"bounds":true,"escape":true,"inline":true,"nil":true}`.
+
+##### **experimentalDiagnosticsDelay** *time.Duration*
+
+**This setting is experimental and may be deleted.**
+
+experimentalDiagnosticsDelay controls the amount of time that gopls waits
+after the most recent file modification before computing deep diagnostics.
+Simple diagnostics (parsing and type-checking) are always run immediately
+on recently modified packages.
+
+This option must be set to a valid duration string, for example `"250ms"`.
+
+Default: `"250ms"`.
+
+#### Documentation
+
+##### **hoverKind** *enum*
+
+hoverKind controls the information that appears in the hover text.
+SingleLine and Structured are intended for use only by authors of editor plugins.
+
+Must be one of:
+
+* `"FullDocumentation"`
+* `"NoDocumentation"`
+* `"SingleLine"`
+* `"Structured"` is an experimental setting that returns a structured hover format.
+This format separates the signature from the documentation, so that the client
+can do more manipulation of these fields.\
+This should only be used by clients that support this behavior.
+
+* `"SynopsisDocumentation"`
+Default: `"FullDocumentation"`.
+
+##### **linkTarget** *string*
+
+linkTarget controls where documentation links go.
+It might be one of:
+
+* `"godoc.org"`
+* `"pkg.go.dev"`
+
+If company chooses to use its own `godoc.org`, its address can be used as well.
+
+Default: `"pkg.go.dev"`.
+
+##### **linksInHover** *bool*
+
+linksInHover toggles the presence of links to documentation in hover.
+
+Default: `true`.
+
+#### Navigation
+
+##### **importShortcut** *enum*
+
+importShortcut specifies whether import statements should link to
+documentation or go to definitions.
+
+Must be one of:
+
+* `"Both"`
+* `"Definition"`
+* `"Link"`
+Default: `"Both"`.
+
+##### **symbolMatcher** *enum*
+
+**This is an advanced setting and should not be configured by most `gopls` users.**
+
+symbolMatcher sets the algorithm that is used when finding workspace symbols.
+
+Must be one of:
+
+* `"CaseInsensitive"`
+* `"CaseSensitive"`
+* `"Fuzzy"`
+Default: `"Fuzzy"`.
+
+##### **symbolStyle** *enum*
+
+**This is an advanced setting and should not be configured by most `gopls` users.**
+
+symbolStyle controls how symbols are qualified in symbol responses.
+
+Example Usage:
+
+```json5
+"gopls": {
+...
+ "symbolStyle": "dynamic",
+...
+}
+```
+
+Must be one of:
+
+* `"Dynamic"` uses whichever qualifier results in the highest scoring
+match for the given symbol query. Here a "qualifier" is any "/" or "."
+delimited suffix of the fully qualified symbol. i.e. "to/pkg.Foo.Field" or
+just "Foo.Field".
+
+* `"Full"` is fully qualified symbols, i.e.
+"path/to/pkg.Foo.Field".
+
+* `"Package"` is package qualified symbols i.e.
+"pkg.Foo.Field".
+
+Default: `"Dynamic"`.
+
+#### **verboseOutput** *bool*
+
+**This setting is for debugging purposes only.**
+
+verboseOutput enables additional debug logging.
+
+Default: `false`.
+
+<!-- END User: DO NOT MANUALLY EDIT THIS SECTION -->
## Code Lenses
-These are the code lenses that `gopls` currently supports. They can be enabled and disabled using the `codeLenses` setting, documented above. The names and features are subject to change.
+These are the code lenses that `gopls` currently supports. They can be enabled
+and disabled using the `codelenses` setting, documented above. Their names and
+features are subject to change.
<!-- BEGIN Lenses: DO NOT MANUALLY EDIT THIS SECTION -->
### **Run go generate**
+
Identifier: `generate`
generate runs `go generate` for a given directory.
-
### **Regenerate cgo**
+
Identifier: `regenerate_cgo`
regenerate_cgo regenerates cgo definitions.
-
### **Run test(s)**
+
Identifier: `test`
test runs `go test` for a specific test function.
-
### **Run go mod tidy**
+
Identifier: `tidy`
tidy runs `go mod tidy` for a module.
-
### **Upgrade dependency**
+
Identifier: `upgrade_dependency`
upgrade_dependency upgrades a dependency.
-
### **Run go mod vendor**
+
Identifier: `vendor`
vendor runs `go mod vendor` for a module.
-
### **Toggle gc_details**
+
Identifier: `gc_details`
gc_details controls calculation of gc annotations.
-
<!-- END Lenses: DO NOT MANUALLY EDIT THIS SECTION -->
diff --git a/gopls/doc/vim.md b/gopls/doc/vim.md
index ea4854f..f05a4e3 100644
--- a/gopls/doc/vim.md
+++ b/gopls/doc/vim.md
@@ -1,6 +1,20 @@
# Vim / Neovim
-## vim-go
+* [vim-go](#vimgo)
+* [LanguageClient-neovim](#lcneovim)
+* [Ale](#ale)
+* [vim-lsp](#vimlsp)
+* [vim-lsc](#vimlsc)
+* [coc.nvim](#cocnvim)
+* [govim](#govim)
+* [Neovim v0.5.0+](#neovim)
+ * [Installation](#neovim-install)
+ * [Custom Configuration](#neovim-config)
+ * [Imports](#neovim-imports)
+ * [Omnifunc](#neovim-omnifunc)
+ * [Additional Links](#neovim-links)
+
+## <a href="#vimgo" id="vimgo">vim-go</a>
Use [vim-go] ver 1.20+, with the following configuration:
@@ -9,7 +23,7 @@
let g:go_info_mode='gopls'
```
-## LanguageClient-neovim
+## <a href="#lcneovim" id="lcneovim">LanguageClient-neovim</a>
Use [LanguageClient-neovim], with the following configuration:
@@ -22,7 +36,7 @@
autocmd BufWritePre *.go :call LanguageClient#textDocument_formatting_sync()
```
-## Ale
+## <a href="#ale" id="ale">Ale</a>
Use [ale]:
@@ -34,7 +48,7 @@
see [this issue][ale-issue-2179]
-## vim-lsp
+## <a href="#vimlsp" id="vimlsp">vim-lsp</a>
Use [prabirshrestha/vim-lsp], with the following configuration:
@@ -53,7 +67,7 @@
augroup END
```
-## vim-lsc
+## <a href="#vimlsc" id="vimlsc">vim-lsc</a>
Use [natebosch/vim-lsc], with the following configuration:
@@ -71,7 +85,7 @@
issues [#180](https://github.com/natebosch/vim-lsc/issues/180) and
[#213](https://github.com/natebosch/vim-lsc/issues/213).
-## coc.nvim
+## <a href="#cocnvim" id="cocnvim">coc.nvim</a>
Use [coc.nvim], with the following `coc-settings.json` configuration:
@@ -96,26 +110,38 @@
autocmd BufWritePre *.go :call CocAction('runCommand', 'editor.action.organizeImport')
```
-## govim
+## <a href="#govim" id="govim">govim</a>
In vim classic only, use the experimental [`govim`], simply follow the [install steps][govim-install].
-## Neovim v0.5.0+
+## <a href="#neovim" id="neovim">Neovim v0.5.0+</a>
To use the new (still experimental) native LSP client in Neovim, make sure you
[install][nvim-install] the prerelease v0.5.0 version of Neovim (aka “nightly”),
the `nvim-lspconfig` configuration helper plugin, and check the
[`gopls` configuration section][nvim-lspconfig] there.
-### Custom configuration
+### <a href="#neovim-install" id="neovim-install">Installation</a>
+
+You can use Neovim's native plugin system. On a Unix system, you can do that by
+cloning the `nvim-lspconfig` repository into the correct directory:
+
+```sh
+dir="${HOME}/.local/share/nvim/site/pack/nvim-lspconfig/opt/nvim-lspconfig/"
+mkdir -p "$dir"
+cd "$dir"
+git clone 'https://github.com/neovim/nvim-lspconfig.git' .
+```
+
+### <a href="#neovim-config" id="neovim-config">Custom Configuration</a>
You can add custom configuration using Lua. Here is an example of enabling the
`unusedparams` check as well as `staticcheck`:
```vim
lua <<EOF
- nvim_lsp = require "lspconfig"
- nvim_lsp.gopls.setup {
+ lspconfig = require "lspconfig"
+ lspconfig.gopls.setup {
cmd = {"gopls", "serve"},
settings = {
gopls = {
@@ -129,7 +155,7 @@
EOF
```
-### Imports
+### <a href="#neovim-imports" id="neovim-imports">Imports</a>
To get your imports ordered on save, like `goimports` does, you can define
a helper function in Lua:
@@ -164,7 +190,7 @@
(Taken from the [discussion][nvim-lspconfig-imports] on Neovim issue tracker.)
-### Omnifunc
+### <a href="#neovim-omnifunc" id="neovim-omnifunc">Omnifunc</a>
To make your <kbd>Ctrl</kbd>+<kbd>x</kbd>,<kbd>Ctrl</kbd>+<kbd>o</kbd> work, add
this to your `init.vim`:
@@ -173,7 +199,7 @@
autocmd FileType go setlocal omnifunc=v:lua.vim.lsp.omnifunc
```
-### Additional Links
+### <a href="#neovim-links" id="neovim-links">Additional Links</a>
* [Neovim's official LSP documentation][nvim-docs].
diff --git a/gopls/go.mod b/gopls/go.mod
index dac3b93..3fac43f 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -3,11 +3,15 @@
go 1.12
require (
+ github.com/jba/templatecheck v0.5.0
github.com/sanity-io/litter v1.3.0
github.com/sergi/go-diff v1.1.0
- golang.org/x/tools v0.0.0-20201217163546-c88dec5c6b05
+ golang.org/x/mod v0.3.0
+ golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
honnef.co/go/tools v0.0.1-2020.1.6
mvdan.cc/gofumpt v0.0.0-20200927160801-5bfeb2e70dd6
mvdan.cc/xurls/v2 v2.2.0
)
+
+replace golang.org/x/tools => ../
diff --git a/gopls/go.sum b/gopls/go.sum
index e135d18..bf8c68f 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -7,6 +7,10 @@
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM=
+github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
+github.com/jba/templatecheck v0.5.0 h1:sZwNjXG3xNApuwKmgUWEo2JuxmG0sgNaELl0zwRQ9x8=
+github.com/jba/templatecheck v0.5.0/go.mod h1:/1k7EajoSErFI9GLHAsiIJEaNLt3ALKNw2TV7z2SYv4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -27,8 +31,6 @@
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -37,30 +39,15 @@
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20201211192254-72fbef54948b h1:8fYBhX5ZQZtb7nVKo58TjndJwMM+cOB1xOnfjgH3uiY=
-golang.org/x/tools v0.0.0-20201211192254-72fbef54948b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201217163546-c88dec5c6b05 h1:4hzTNH658xirWc4MvAx9HK2/2hY6z42i1+lRK8OzeJE=
-golang.org/x/tools v0.0.0-20201217163546-c88dec5c6b05/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go
index e2a5af5..46d7dd7 100644
--- a/gopls/internal/hooks/diff.go
+++ b/gopls/internal/hooks/diff.go
@@ -5,14 +5,25 @@
package hooks
import (
+ "fmt"
+
"github.com/sergi/go-diff/diffmatchpatch"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span"
)
-func ComputeEdits(uri span.URI, before, after string) []diff.TextEdit {
+func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit, err error) {
+ // The go-diff library has an unresolved panic (see golang/go#278774).
+ // TOOD(rstambler): Remove the recover once the issue has been fixed
+ // upstream.
+ defer func() {
+ if r := recover(); r != nil {
+ edits = nil
+ err = fmt.Errorf("unable to compute edits for %s: %s", uri.Filename(), r)
+ }
+ }()
diffs := diffmatchpatch.New().DiffMain(before, after, true)
- edits := make([]diff.TextEdit, 0, len(diffs))
+ edits = make([]diff.TextEdit, 0, len(diffs))
offset := 0
for _, d := range diffs {
start := span.NewPoint(0, 0, offset)
@@ -26,5 +37,5 @@
edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.Point{}), NewText: d.Text})
}
}
- return edits
+ return edits, nil
}
diff --git a/gopls/internal/regtest/codelens_test.go b/gopls/internal/regtest/codelens_test.go
index 8935984..a58de79 100644
--- a/gopls/internal/regtest/codelens_test.go
+++ b/gopls/internal/regtest/codelens_test.go
@@ -146,7 +146,7 @@
require golang.org/x/hello v1.3.3
`
if got != wantGoMod {
- t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(wantGoMod, got))
+ t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(t, wantGoMod, got))
}
})
})
@@ -208,7 +208,7 @@
require golang.org/x/hello v1.0.0
`
if got != wantGoMod {
- t.Fatalf("go.mod tidy failed:\n%s", tests.Diff(wantGoMod, got))
+ t.Fatalf("go.mod tidy failed:\n%s", tests.Diff(t, wantGoMod, got))
}
}, ProxyFiles(proxy))
}
diff --git a/gopls/internal/regtest/completion_test.go b/gopls/internal/regtest/completion_test.go
index 6c486f6..3d33799 100644
--- a/gopls/internal/regtest/completion_test.go
+++ b/gopls/internal/regtest/completion_test.go
@@ -213,3 +213,60 @@
return ""
}
+
+func TestUnimportedCompletion(t *testing.T) {
+ testenv.NeedsGo1Point(t, 14)
+
+ const mod = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func main() {
+ _ = blah
+}
+`
+ withOptions(
+ ProxyFiles(proxy),
+ ).run(t, mod, func(t *testing.T, env *Env) {
+ // Explicitly download example.com so it's added to the module cache
+ // and offered as an unimported completion.
+ env.RunGoCommand("get", "example.com@v1.2.3")
+ env.RunGoCommand("mod", "tidy")
+
+ // Trigger unimported completions for the example.com/blah package.
+ env.OpenFile("main.go")
+ 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])
+
+ // Trigger completions once again for the blah.<> selector.
+ env.RegexpReplace("main.go", "_ = blah", "_ = blah.")
+ env.Await(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2),
+ )
+ pos = env.RegexpSearch("main.go", "\n}")
+ completions = env.Completion("main.go", pos)
+ if len(completions.Items) != 1 {
+ t.Fatalf("expected 1 completion item, got %v", len(completions.Items))
+ }
+ item := completions.Items[0]
+ if item.Label != "Name" {
+ t.Fatalf("expected completion item blah.Name, got %v", item.Label)
+ }
+ env.AcceptCompletion("main.go", pos, item)
+
+ // Await the diagnostics to add example.com/blah to the go.mod file.
+ env.SaveBufferWithoutActions("main.go")
+ env.Await(
+ env.DiagnosticAtRegexp("go.mod", "module mod.com"),
+ env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
+ )
+ })
+}
diff --git a/gopls/internal/regtest/definition_test.go b/gopls/internal/regtest/definition_test.go
index 4daa45f..36a9352 100644
--- a/gopls/internal/regtest/definition_test.go
+++ b/gopls/internal/regtest/definition_test.go
@@ -128,7 +128,7 @@
}
want := "```go\nfunc (error).Error() string\n```"
if content.Value != want {
- t.Fatalf("hover failed:\n%s", tests.Diff(want, content.Value))
+ t.Fatalf("hover failed:\n%s", tests.Diff(t, want, content.Value))
}
})
}
diff --git a/gopls/internal/regtest/diagnostics_test.go b/gopls/internal/regtest/diagnostics_test.go
index 0c5c99e..50bb593 100644
--- a/gopls/internal/regtest/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics_test.go
@@ -531,7 +531,7 @@
env.SaveBuffer("main.go")
fixed := env.ReadWorkspaceFile("main.go")
if original != fixed {
- t.Fatalf("generated file was changed by quick fixes:\n%s", tests.Diff(original, fixed))
+ t.Fatalf("generated file was changed by quick fixes:\n%s", tests.Diff(t, original, fixed))
}
})
}
@@ -760,9 +760,7 @@
runner.Run(t, simplePackage, func(t *testing.T, env *Env) {
env.OpenFile("a/a1.go")
env.CreateBuffer("a/a2.go", ``)
- if err := env.Editor.SaveBufferWithoutActions(env.Ctx, "a/a2.go"); err != nil {
- t.Fatal(err)
- }
+ env.SaveBufferWithoutActions("a/a2.go")
env.Await(
OnceMet(
CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
@@ -825,7 +823,7 @@
env.CreateBuffer("hello/hello_x_test.go", ``)
// Save the empty file (no actions since formatting will fail).
- env.Editor.SaveBufferWithoutActions(env.Ctx, "hello/hello_x_test.go")
+ env.SaveBufferWithoutActions("hello/hello_x_test.go")
// Add the content. The missing import is for the package under test.
env.EditBuffer("hello/hello_x_test.go", fake.NewEdit(0, 0, 0, 0, `package hello_test
@@ -1177,7 +1175,7 @@
env.CreateBuffer("main.go", "")
env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
- env.Editor.SaveBufferWithoutActions(env.Ctx, "main.go")
+ env.SaveBufferWithoutActions("main.go")
env.Await(
CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
@@ -1515,10 +1513,14 @@
go 1.hello
`))
+ // As of golang/go#42529, go.mod changes do not reload the workspace until
+ // they are saved.
+ env.SaveBufferWithoutActions("go.mod")
env.Await(
OutstandingWork(lsp.WorkspaceLoadFailure, "invalid go version"),
)
env.RegexpReplace("go.mod", "go 1.hello", "go 1.12")
+ env.SaveBufferWithoutActions("go.mod")
env.Await(
NoOutstandingWork(),
)
diff --git a/gopls/internal/regtest/fix_test.go b/gopls/internal/regtest/fix_test.go
index 887f282..e513148 100644
--- a/gopls/internal/regtest/fix_test.go
+++ b/gopls/internal/regtest/fix_test.go
@@ -58,7 +58,7 @@
}
`
if got := env.Editor.BufferText("main.go"); got != want {
- t.Fatalf("TestFillStruct failed:\n%s", tests.Diff(want, got))
+ t.Fatalf("TestFillStruct failed:\n%s", tests.Diff(t, want, got))
}
})
}
diff --git a/gopls/internal/regtest/formatting_test.go b/gopls/internal/regtest/formatting_test.go
index e6da9dc..c9c00da 100644
--- a/gopls/internal/regtest/formatting_test.go
+++ b/gopls/internal/regtest/formatting_test.go
@@ -32,7 +32,7 @@
got := env.Editor.BufferText("main.go")
want := env.ReadWorkspaceFile("main.go.golden")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(want, got))
+ t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -54,7 +54,7 @@
got := env.Editor.BufferText("a.go")
want := env.ReadWorkspaceFile("a.go.formatted")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(want, got))
+ t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -78,7 +78,7 @@
got := env.Editor.BufferText("a.go")
want := env.ReadWorkspaceFile("a.go.imported")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(want, got))
+ t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -99,7 +99,7 @@
got := env.Editor.BufferText("a.go")
want := env.ReadWorkspaceFile("a.go.imported")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(want, got))
+ t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -145,7 +145,7 @@
got := env.Editor.BufferText("main.go")
want := env.ReadWorkspaceFile("main.go.organized")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(want, got))
+ t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -157,7 +157,7 @@
got := env.Editor.BufferText("main.go")
want := env.ReadWorkspaceFile("main.go.formatted")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(want, got))
+ t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -229,7 +229,7 @@
got := env.Editor.BufferText("main.go")
got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity
if tt.want != got {
- t.Errorf("unexpected content after save:\n%s", tests.Diff(tt.want, got))
+ t.Errorf("unexpected content after save:\n%s", tests.Diff(t, tt.want, got))
}
})
})
diff --git a/gopls/internal/regtest/modfile_test.go b/gopls/internal/regtest/modfile_test.go
index f9c705e..0f79646 100644
--- a/gopls/internal/regtest/modfile_test.go
+++ b/gopls/internal/regtest/modfile_test.go
@@ -5,6 +5,7 @@
package regtest
import (
+ "path/filepath"
"strings"
"testing"
@@ -68,7 +69,7 @@
env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""),
)
if got := env.ReadWorkspaceFile("go.mod"); got != goModContent {
- t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(goModContent, got))
+ t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got))
}
// Save the buffer, which will format and organize imports.
// Confirm that the go.mod file still does not change.
@@ -77,7 +78,7 @@
env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""),
)
if got := env.ReadWorkspaceFile("go.mod"); got != goModContent {
- t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(goModContent, got))
+ t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got))
}
})
})
@@ -104,7 +105,7 @@
env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""),
)
if got := env.ReadWorkspaceFile("go.mod"); got != goModContent {
- t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(goModContent, got))
+ t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got))
}
})
})
@@ -153,7 +154,7 @@
}
env.ApplyQuickFixes("main.go", []protocol.Diagnostic{goGetDiag})
if got := env.ReadWorkspaceFile("go.mod"); got != want {
- t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(want, got))
+ t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -200,7 +201,7 @@
}
env.ApplyQuickFixes("main.go", []protocol.Diagnostic{randomDiag})
if got := env.ReadWorkspaceFile("go.mod"); got != want {
- t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(want, got))
+ t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -243,7 +244,7 @@
)
env.ApplyQuickFixes("go.mod", d.Diagnostics)
if got := env.Editor.BufferText("go.mod"); got != want {
- t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(want, got))
+ t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -285,7 +286,7 @@
)
env.ApplyQuickFixes("go.mod", d.Diagnostics)
if got := env.ReadWorkspaceFile("go.mod"); got != want {
- t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(want, got))
+ t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -353,7 +354,7 @@
)
`
if got := env.ReadWorkspaceFile("go.mod"); got != want {
- t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", tests.Diff(want, got))
+ t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -456,7 +457,7 @@
`
env.Await(EmptyDiagnostics("go.mod"))
if got := env.Editor.BufferText("go.mod"); got != want {
- t.Fatalf("suggested fixes failed:\n%s", tests.Diff(want, got))
+ t.Fatalf("suggested fixes failed:\n%s", tests.Diff(t, want, got))
}
})
}
@@ -630,7 +631,7 @@
)
got := env.ReadWorkspaceFile("go.mod")
if got != original {
- t.Fatalf("go.mod file modified:\n%s", tests.Diff(original, got))
+ t.Fatalf("go.mod file modified:\n%s", tests.Diff(t, original, got))
}
env.RunGoCommand("get", "example.com/blah@v1.2.3")
env.RunGoCommand("mod", "tidy")
@@ -730,3 +731,265 @@
)
})
}
+
+func TestSumUpdateFixesDiagnostics(t *testing.T) {
+ testenv.NeedsGo1Point(t, 14)
+
+ const mod = `
+-- go.mod --
+module mod.com
+
+go 1.12
+
+require (
+ example.com v1.2.3
+)
+-- go.sum --
+-- main.go --
+package main
+
+import (
+ "example.com/blah"
+)
+
+func main() {
+ println(blah.Name)
+}
+`
+ withOptions(
+ Modes(Singleton), // workspace modules don't use -mod=readonly (golang/go#43346)
+ ProxyFiles(workspaceProxy),
+ ).run(t, mod, func(t *testing.T, env *Env) {
+ d := &protocol.PublishDiagnosticsParams{}
+ env.OpenFile("go.mod")
+ env.Await(
+ OnceMet(
+ DiagnosticAt("go.mod", 0, 0),
+ ReadDiagnostics("go.mod", d),
+ ),
+ )
+ env.ApplyQuickFixes("go.mod", d.Diagnostics)
+ env.Await(
+ EmptyDiagnostics("go.mod"),
+ )
+ })
+}
+
+// This test confirms that editing a go.mod file only causes metadata
+// to be invalidated when it's saved.
+func TestGoModInvalidatesOnSave(t *testing.T) {
+ const mod = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func main() {
+ hello()
+}
+-- hello.go --
+package main
+
+func hello() {}
+`
+ 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) {
+ env.OpenFile("go.mod")
+ env.RegexpReplace("go.mod", "module", "modul")
+ // Confirm that we still have metadata with only on-disk edits.
+ env.OpenFile("main.go")
+ file, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "hello"))
+ if filepath.Base(file) != "hello.go" {
+ t.Fatalf("expected definition in hello.go, got %s", file)
+ }
+ // Confirm that we no longer have metadata when the file is saved.
+ env.SaveBufferWithoutActions("go.mod")
+ _, _, err := env.Editor.GoToDefinition(env.Ctx, "main.go", env.RegexpSearch("main.go", "hello"))
+ if err == nil {
+ t.Fatalf("expected error, got none")
+ }
+ })
+}
+
+func TestRemoveUnusedDependency(t *testing.T) {
+ testenv.NeedsGo1Point(t, 14)
+
+ const proxy = `
+-- hasdep.com@v1.2.3/go.mod --
+module hasdep.com
+
+go 1.12
+
+require example.com v1.2.3
+-- hasdep.com@v1.2.3/a/a.go --
+package a
+-- 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.com@v1.2.3/go.mod --
+module random.com
+
+go 1.12
+-- random.com@v1.2.3/blah/blah.go --
+package blah
+
+const Name = "Blah"
+`
+ t.Run("almost tidied", func(t *testing.T) {
+ const mod = `
+-- go.mod --
+module mod.com
+
+go 1.12
+
+require hasdep.com v1.2.3
+-- go.sum --
+example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
+example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
+hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI=
+hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes=
+-- main.go --
+package main
+
+func main() {}
+`
+ withOptions(
+ ProxyFiles(proxy),
+ ).run(t, mod, func(t *testing.T, env *Env) {
+ d := &protocol.PublishDiagnosticsParams{}
+ env.Await(
+ OnceMet(
+ env.DiagnosticAtRegexp("go.mod", "require hasdep.com v1.2.3"),
+ ReadDiagnostics("go.mod", d),
+ ),
+ )
+ const want = `module mod.com
+
+go 1.12
+`
+ env.ApplyQuickFixes("go.mod", d.Diagnostics)
+ if got := env.ReadWorkspaceFile("go.mod"); got != want {
+ t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got))
+ }
+ })
+ })
+
+ t.Run("not tidied", func(t *testing.T) {
+ const mod = `
+-- go.mod --
+module mod.com
+
+go 1.12
+
+require hasdep.com v1.2.3
+require random.com v1.2.3
+-- go.sum --
+example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
+example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
+hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI=
+hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes=
+random.com v1.2.3 h1:PzYTykzqqH6+qU0dIgh9iPFbfb4Mm8zNBjWWreRKtx0=
+random.com v1.2.3/go.mod h1:8EGj+8a4Hw1clAp8vbaeHAsKE4sbm536FP7nKyXO+qQ=
+-- main.go --
+package main
+
+func main() {}
+`
+ withOptions(
+ ProxyFiles(proxy),
+ ).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")
+ env.Await(
+ OnceMet(
+ DiagnosticAt("go.mod", pos.Line, pos.Column),
+ ReadDiagnostics("go.mod", d),
+ ),
+ )
+ const want = `module mod.com
+
+go 1.12
+
+require random.com v1.2.3
+`
+ var diagnostics []protocol.Diagnostic
+ for _, d := range d.Diagnostics {
+ if d.Range.Start.Line != float64(pos.Line) {
+ continue
+ }
+ diagnostics = append(diagnostics, d)
+ }
+ env.ApplyQuickFixes("go.mod", diagnostics)
+ if got := env.Editor.BufferText("go.mod"); got != want {
+ t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got))
+ }
+ })
+ })
+}
+
+func TestSumUpdateQuickFix(t *testing.T) {
+ // Error messages changed in 1.16 that changed the diagnostic positions.
+ testenv.NeedsGo1Point(t, 16)
+
+ const mod = `
+-- go.mod --
+module mod.com
+
+go 1.12
+
+require (
+ example.com v1.2.3
+)
+-- go.sum --
+-- main.go --
+package main
+
+import (
+ "example.com/blah"
+)
+
+func main() {
+ blah.Hello()
+}
+`
+ withOptions(
+ ProxyFiles(workspaceProxy),
+ Modes(Singleton),
+ ).run(t, mod, func(t *testing.T, env *Env) {
+ env.OpenFile("go.mod")
+ pos := env.RegexpSearch("go.mod", "example.com")
+ params := &protocol.PublishDiagnosticsParams{}
+ env.Await(
+ OnceMet(
+ env.DiagnosticAtRegexp("go.mod", "example.com"),
+ ReadDiagnostics("go.mod", params),
+ ),
+ )
+ var diagnostic protocol.Diagnostic
+ for _, d := range params.Diagnostics {
+ if d.Range.Start.Line == float64(pos.Line) {
+ diagnostic = d
+ break
+ }
+ }
+ env.ApplyQuickFixes("go.mod", []protocol.Diagnostic{diagnostic})
+ const want = `example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c=
+example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
+`
+ if got := env.ReadWorkspaceFile("go.sum"); got != want {
+ t.Fatalf("unexpected go.sum contents:\n%s", tests.Diff(t, want, got))
+ }
+ })
+}
diff --git a/gopls/internal/regtest/runner.go b/gopls/internal/regtest/runner.go
index 7716d79..627a6fc 100644
--- a/gopls/internal/regtest/runner.go
+++ b/gopls/internal/regtest/runner.go
@@ -323,6 +323,8 @@
"openbsd-amd64-64": "golang.org/issues/42789",
"openbsd-386-64": "golang.org/issues/42789",
"openbsd-386-68": "golang.org/issues/42789",
+ "openbsd-amd64-68": "golang.org/issues/42789",
+ "linux-arm": "golang.org/issues/43355",
"darwin-amd64-10_12": "",
"freebsd-amd64-race": "",
"illumos-amd64": "",
diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go
index c65bba9..5472176 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace_test.go
@@ -387,7 +387,7 @@
),
)
env.RegexpReplace("modb/go.mod", "modul", "module")
- env.Editor.SaveBufferWithoutActions(env.Ctx, "modb/go.mod")
+ env.SaveBufferWithoutActions("modb/go.mod")
env.Await(
env.DiagnosticAtRegexp("modb/b/b.go", "x"),
)
@@ -443,14 +443,26 @@
// loaded. Validate this by jumping to a definition in b.com and ensuring
// that we go to the module cache.
env.OpenFile("moda/a/a.go")
- location, _ := 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(location, want) {
- t.Errorf("expected %s, got %v", want, location)
+ env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+
+ // To verify which modules are loaded, we'll jump to the definition of
+ // b.Hello.
+ checkHelloLocation := func(want string) error {
+ location, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if !strings.HasSuffix(location, want) {
+ return fmt.Errorf("expected %s, got %v", want, location)
+ }
+ return nil
}
- workdir := env.Sandbox.Workdir.RootURI().SpanURI().Filename()
+
+ // Initially this should be in the module cache, as b.com is not replaced.
+ if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil {
+ t.Fatal(err)
+ }
// Now, modify the gopls.mod file on disk to activate the b.com module in
// the workspace.
+ workdir := env.Sandbox.Workdir.RootURI().SpanURI().Filename()
env.WriteWorkspaceFile("gopls.mod", fmt.Sprintf(`module gopls-workspace
require (
@@ -465,6 +477,7 @@
// Check that go.mod diagnostics picked up the newly active mod file.
// The local version of modb has an extra dependency we need to download.
env.OpenFile("modb/go.mod")
+ env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2))
var d protocol.PublishDiagnosticsParams
env.Await(
OnceMet(
@@ -475,14 +488,14 @@
env.ApplyQuickFixes("modb/go.mod", d.Diagnostics)
env.Await(env.DiagnosticAtRegexp("modb/b/b.go", "x"))
// Jumping to definition should now go to b.com in the workspace.
- location, _ = env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
- if want := "modb/b/b.go"; !strings.HasSuffix(location, want) {
- t.Errorf("expected %s, got %v", want, location)
+ if err := checkHelloLocation("modb/b/b.go"); err != nil {
+ t.Fatal(err)
}
// Now, let's modify the gopls.mod *overlay* (not on disk), and verify that
- // this change is also picked up.
+ // this change is only picked up once it is saved.
env.OpenFile("gopls.mod")
+ env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 3))
env.SetBufferContent("gopls.mod", fmt.Sprintf(`module gopls-workspace
require (
@@ -491,15 +504,21 @@
replace a.com => %s/moda/a
`, workdir))
+
+ // Editing the gopls.mod removes modb from the workspace modules, and so
+ // should clear outstanding diagnostics...
env.Await(OnceMet(
CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2),
EmptyDiagnostics("modb/go.mod"),
))
-
- // Just as before, check that we now jump to the module cache.
- location, _ = 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(location, want) {
- t.Errorf("expected %s, got %v", want, location)
+ // ...but does not yet cause a workspace reload, so we should still jump to modb.
+ if err := checkHelloLocation("modb/b/b.go"); err != nil {
+ t.Fatal(err)
+ }
+ // Saving should reload the workspace.
+ env.SaveBufferWithoutActions("gopls.mod")
+ if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil {
+ t.Fatal(err)
}
})
}
@@ -533,7 +552,7 @@
-- moda/a/go.mod --
module a.com
-require b.com/v2 v2.0.0
+require b.com/v2 v2.1.9
-- moda/a/a.go --
package a
@@ -708,6 +727,11 @@
go 1.12
require exclude.com v1.0.0
+
+-- include/go.sum --
+exclude.com v1.0.0 h1:Q5QSfDXY5qyNCBeUiWovUGqcLCRZKoTs9XdBeVz+w1I=
+exclude.com v1.0.0/go.mod h1:hFox2uDlNB2s2Jfd9tHlQVfgqUiLVTmh6ZKat4cvnj4=
+
-- include/include.go --
package include
diff --git a/gopls/internal/regtest/wrappers.go b/gopls/internal/regtest/wrappers.go
index 071fe78..849a1a6 100644
--- a/gopls/internal/regtest/wrappers.go
+++ b/gopls/internal/regtest/wrappers.go
@@ -6,6 +6,7 @@
import (
"io"
+ "path"
"testing"
"golang.org/x/tools/internal/lsp/fake"
@@ -146,6 +147,13 @@
}
}
+func (e *Env) SaveBufferWithoutActions(name string) {
+ e.T.Helper()
+ if err := e.Editor.SaveBufferWithoutActions(e.Ctx, name); err != nil {
+ e.T.Fatal(err)
+ }
+}
+
// GoToDefinition goes to definition in the editor, calling t.Fatal on any
// error.
func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) {
@@ -247,10 +255,14 @@
}
}
-func (e *Env) DumpGoSum() {
+func (e *Env) DumpGoSum(dir string) {
e.T.Helper()
- e.RunGoCommand("list", "-mod=mod", "...")
- e.T.Log("\n\n-- go.sum --\n" + e.ReadWorkspaceFile("go.sum"))
+
+ if err := e.Sandbox.RunGoCommand(e.Ctx, dir, "list", []string{"-mod=mod", "..."}); err != nil {
+ e.T.Fatal(err)
+ }
+ sumFile := path.Join(dir, "/go.sum")
+ e.T.Log("\n\n-- " + sumFile + " --\n" + e.ReadWorkspaceFile(sumFile))
e.T.Fatal("see contents above")
}
@@ -319,6 +331,15 @@
return completions
}
+// AcceptCompletion accepts a completion for the given item at the given
+// position.
+func (e *Env) AcceptCompletion(path string, pos fake.Pos, item protocol.CompletionItem) {
+ e.T.Helper()
+ if err := e.Editor.AcceptCompletion(e.Ctx, path, pos, item); err != nil {
+ e.T.Fatal(err)
+ }
+}
+
// CodeAction calls testDocument/codeAction for the given path, and calls
// t.Fatal if there are errors.
func (e *Env) CodeAction(path string) []protocol.CodeAction {
diff --git a/gopls/release/release.go b/gopls/release/release.go
new file mode 100644
index 0000000..62455fe
--- /dev/null
+++ b/gopls/release/release.go
@@ -0,0 +1,213 @@
+// 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 release checks that the a given version of gopls is ready for
+// release. It can also tag and publish the release.
+//
+// To run:
+//
+// $ cd $GOPATH/src/golang.org/x/tools/gopls
+// $ go run release/release.go -version=<version>
+package main
+
+import (
+ "flag"
+ "fmt"
+ "go/types"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/mod/semver"
+ "golang.org/x/tools/go/packages"
+)
+
+var (
+ versionFlag = flag.String("version", "", "version to tag")
+ remoteFlag = flag.String("remote", "", "remote to which to push the tag")
+ releaseFlag = flag.Bool("release", false, "release is true if you intend to tag and push a release")
+)
+
+func main() {
+ flag.Parse()
+
+ if *versionFlag == "" {
+ log.Fatalf("must provide -version flag")
+ }
+ if !semver.IsValid(*versionFlag) {
+ log.Fatalf("invalid version %s", *versionFlag)
+ }
+ if semver.Major(*versionFlag) != "v0" {
+ log.Fatalf("expected major version v0, got %s", semver.Major(*versionFlag))
+ }
+ if semver.Build(*versionFlag) != "" {
+ log.Fatalf("unexpected build suffix: %s", *versionFlag)
+ }
+ if *releaseFlag && *remoteFlag == "" {
+ log.Fatalf("must provide -remote flag if releasing")
+ }
+ user, err := user.Current()
+ if err != nil {
+ log.Fatal(err)
+ }
+ // Validate that the user is running the program from the gopls module.
+ wd, err := os.Getwd()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if filepath.Base(wd) != "gopls" {
+ log.Fatalf("must run from the gopls module")
+ }
+ // Confirm that they are running on a branch with a name following the
+ // format of "gopls-release-branch.<major>.<minor>".
+ if err := validateBranchName(*versionFlag); err != nil {
+ log.Fatal(err)
+ }
+ // Confirm that they have updated the hardcoded version.
+ if err := validateHardcodedVersion(wd, *versionFlag); err != nil {
+ log.Fatal(err)
+ }
+ // Confirm that the versions in the go.mod file are correct.
+ if err := validateGoModFile(wd); err != nil {
+ log.Fatal(err)
+ }
+ earlyExitMsg := "Validated that the release is ready. Exiting without tagging and publishing."
+ if !*releaseFlag {
+ fmt.Println(earlyExitMsg)
+ os.Exit(0)
+ }
+ fmt.Println(`Proceeding to tagging and publishing the release...
+Please enter Y if you wish to proceed or anything else if you wish to exit.`)
+ // Accept and process user input.
+ var input string
+ fmt.Scanln(&input)
+ switch input {
+ case "Y":
+ fmt.Println("Proceeding to tagging and publishing the release.")
+ default:
+ fmt.Println(earlyExitMsg)
+ os.Exit(0)
+ }
+ // To tag the release:
+ // $ git -c user.email=username@google.com tag -a -m “<message>” gopls/v<major>.<minor>.<patch>-<pre-release>
+ goplsVersion := fmt.Sprintf("gopls/%s", *versionFlag)
+ cmd := exec.Command("git", "-c", fmt.Sprintf("user.email=%s@google.com", user.Username), "tag", "-a", "-m", fmt.Sprintf("%q", goplsVersion), goplsVersion)
+ if err := cmd.Run(); err != nil {
+ log.Fatal(err)
+ }
+ // Push the tag to the remote:
+ // $ git push <remote> gopls/v<major>.<minor>.<patch>-pre.1
+ cmd = exec.Command("git", "push", *remoteFlag, goplsVersion)
+ if err := cmd.Run(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+// validateBranchName reports whether the user's current branch name is of the
+// form "gopls-release-branch.<major>.<minor>". It reports an error if not.
+func validateBranchName(version string) error {
+ cmd := exec.Command("git", "branch", "--show-current")
+ stdout, err := cmd.Output()
+ if err != nil {
+ return err
+ }
+ branch := strings.TrimSpace(string(stdout))
+ expectedBranch := fmt.Sprintf("gopls-release-branch.%s", strings.TrimPrefix(semver.MajorMinor(version), "v"))
+ if branch != expectedBranch {
+ return fmt.Errorf("expected release branch %s, got %s", expectedBranch, branch)
+ }
+ return nil
+}
+
+// validateHardcodedVersion reports whether the version hardcoded in the gopls
+// binary is equivalent to the version being published. It reports an error if
+// not.
+func validateHardcodedVersion(wd string, version string) error {
+ pkgs, err := packages.Load(&packages.Config{
+ Dir: filepath.Dir(wd),
+ Mode: packages.NeedName | packages.NeedFiles |
+ packages.NeedCompiledGoFiles | packages.NeedImports |
+ packages.NeedTypes | packages.NeedTypesSizes,
+ }, "golang.org/x/tools/internal/lsp/debug")
+ if err != nil {
+ return err
+ }
+ if len(pkgs) != 1 {
+ return fmt.Errorf("expected 1 package, got %v", len(pkgs))
+ }
+ pkg := pkgs[0]
+ obj := pkg.Types.Scope().Lookup("Version")
+ c, ok := obj.(*types.Const)
+ if !ok {
+ return fmt.Errorf("no constant named Version")
+ }
+ hardcodedVersion, err := strconv.Unquote(c.Val().ExactString())
+ if err != nil {
+ return err
+ }
+ if semver.Prerelease(hardcodedVersion) != "" {
+ return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion)
+ }
+ // Don't worry about pre-release tags and expect that there is no build
+ // suffix.
+ version = strings.TrimSuffix(version, semver.Prerelease(version))
+ if hardcodedVersion != version {
+ return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion)
+ }
+ return nil
+}
+
+func validateGoModFile(wd string) error {
+ filename := filepath.Join(wd, "go.mod")
+ data, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return err
+ }
+ gomod, err := modfile.Parse(filename, data, nil)
+ if err != nil {
+ return err
+ }
+ // Confirm that there is no replace directive in the go.mod file.
+ if len(gomod.Replace) > 0 {
+ return fmt.Errorf("expected no replace directives, got %v", len(gomod.Replace))
+ }
+ // Confirm that the version of x/tools in the gopls/go.mod file points to
+ // the second-to-last commit. (The last commit will be the one to update the
+ // go.mod file.)
+ cmd := exec.Command("git", "rev-parse", "@~")
+ stdout, err := cmd.Output()
+ if err != nil {
+ return err
+ }
+ hash := string(stdout)
+ // Find the golang.org/x/tools require line and compare the versions.
+ var version string
+ for _, req := range gomod.Require {
+ if req.Mod.Path == "golang.org/x/tools" {
+ version = req.Mod.Version
+ break
+ }
+ }
+ if version == "" {
+ return fmt.Errorf("no require for golang.org/x/tools")
+ }
+ split := strings.Split(version, "-")
+ if len(split) != 3 {
+ return fmt.Errorf("unexpected pseudoversion format %s", version)
+ }
+ last := split[len(split)-1]
+ if last == "" {
+ return fmt.Errorf("unexpected pseudoversion format %s", version)
+ }
+ if !strings.HasPrefix(hash, last) {
+ return fmt.Errorf("golang.org/x/tools pseudoversion should be at commit %s, instead got %s", hash, last)
+ }
+ return nil
+}
diff --git a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go
new file mode 100644
index 0000000..72aaff4
--- /dev/null
+++ b/gopls/test/debug/debug_test.go
@@ -0,0 +1,177 @@
+package debug_test
+
+// Provide 'static type checking' of the templates. This guards against changes is various
+// gopls datastructures causing template execution to fail. The checking is done by
+// the github.com/jba/templatecheck pacakge. Before that is run, the test checks that
+// its list of templates and their arguments corresponds to the arguments in
+// calls to render(). The test assumes that all uses of templates are done through render().
+
+import (
+ "go/ast"
+ "html/template"
+ "log"
+ "runtime"
+ "sort"
+ "strings"
+ "testing"
+
+ "github.com/jba/templatecheck"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/lsp/cache"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+)
+
+type tdata struct {
+ tmpl *template.Template
+ data interface{} // a value of the needed type
+}
+
+var templates = map[string]tdata{
+ "MainTmpl": {debug.MainTmpl, &debug.Instance{}},
+ "DebugTmpl": {debug.DebugTmpl, nil},
+ "RPCTmpl": {debug.RPCTmpl, &debug.Rpcs{}},
+ "TraceTmpl": {debug.TraceTmpl, debug.TraceResults{}},
+ "CacheTmpl": {debug.CacheTmpl, &cache.Cache{}},
+ "SessionTmpl": {debug.SessionTmpl, &cache.Session{}},
+ "ViewTmpl": {debug.ViewTmpl, &cache.View{}},
+ "ClientTmpl": {debug.ClientTmpl, &debug.Client{}},
+ "ServerTmpl": {debug.ServerTmpl, &debug.Server{}},
+ //"FileTmpl": {FileTmpl, source.Overlay{}}, // need to construct a source.Overlay in init
+ "InfoTmpl": {debug.InfoTmpl, "something"},
+ "MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}},
+}
+
+// construct a source.Overlay for fileTmpl
+type fakeOverlay struct{}
+
+func (fakeOverlay) Version() float64 {
+ return 0
+}
+func (fakeOverlay) Session() string {
+ return ""
+}
+func (fakeOverlay) VersionedFileIdentity() source.VersionedFileIdentity {
+ return source.VersionedFileIdentity{}
+}
+func (fakeOverlay) FileIdentity() source.FileIdentity {
+ return source.FileIdentity{}
+}
+func (fakeOverlay) Kind() source.FileKind {
+ return 0
+}
+func (fakeOverlay) Read() ([]byte, error) {
+ return nil, nil
+}
+func (fakeOverlay) Saved() bool {
+ return true
+}
+func (fakeOverlay) URI() span.URI {
+ return ""
+}
+
+var _ source.Overlay = fakeOverlay{}
+
+func init() {
+ log.SetFlags(log.Lshortfile)
+ var v fakeOverlay
+ templates["FileTmpl"] = tdata{debug.FileTmpl, v}
+}
+
+func TestTemplates(t *testing.T) {
+ if runtime.GOOS == "android" {
+ t.Skip("this test is not supported for Android")
+ }
+ cfg := &packages.Config{
+ Mode: packages.NeedTypesInfo | packages.LoadAllSyntax, // figure out what's necessary PJW
+ }
+ pkgs, err := packages.Load(cfg, "golang.org/x/tools/internal/lsp/debug")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(pkgs) != 1 {
+ t.Fatalf("expected a single package, but got %d", len(pkgs))
+ }
+ p := pkgs[0]
+ if len(p.Errors) != 0 {
+ t.Fatalf("compiler error, e.g. %v", p.Errors[0])
+ }
+ // find the calls to render in serve.go
+ tree := treeOf(p, "serve.go")
+ if tree == nil {
+ t.Fatalf("found no syntax tree for %s", "serve.go")
+ }
+ renders := callsOf(p, tree, "render")
+ if len(renders) == 0 {
+ t.Fatalf("found no calls to render")
+ }
+ var found = make(map[string]bool)
+ for _, r := range renders {
+ if len(r.Args) != 2 {
+ // template, func
+ t.Fatalf("got %d args, expected 2", len(r.Args))
+ }
+ t0, ok := p.TypesInfo.Types[r.Args[0]]
+ if !ok || !t0.IsValue() || t0.Type.String() != "*html/template.Template" {
+ t.Fatalf("no type info for template")
+ }
+ if id, ok := r.Args[0].(*ast.Ident); !ok {
+ t.Errorf("expected *ast.Ident, got %T", r.Args[0])
+ } else {
+ found[id.Name] = true
+ }
+ }
+ // make sure found and templates have the same templates
+ for k := range found {
+ if _, ok := templates[k]; !ok {
+ t.Errorf("code has template %s, but test does not", k)
+ }
+ }
+ for k := range templates {
+ if _, ok := found[k]; !ok {
+ t.Errorf("test has template %s, code does not", k)
+ }
+ }
+ // now check all the known templates, in alphabetic order, for determinacy
+ keys := []string{}
+ for k := range templates {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ v := templates[k]
+ // the FuncMap is an annoyance; should not be necessary
+ if err := templatecheck.CheckHTML(v.tmpl, v.data); err != nil {
+ t.Errorf("%s: %v", k, err)
+ }
+ }
+}
+
+func callsOf(p *packages.Package, tree *ast.File, name string) []*ast.CallExpr {
+ var ans []*ast.CallExpr
+ f := func(n ast.Node) bool {
+ x, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+ if y, ok := x.Fun.(*ast.Ident); ok {
+ if y.Name == name {
+ ans = append(ans, x)
+ }
+ }
+ return true
+ }
+ ast.Inspect(tree, f)
+ return ans
+}
+func treeOf(p *packages.Package, fname string) *ast.File {
+ for _, tree := range p.Syntax {
+ loc := tree.Package
+ pos := p.Fset.PositionFor(loc, false)
+ if strings.HasSuffix(pos.Filename, fname) {
+ return tree
+ }
+ }
+ return nil
+}
diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go
index 872f1d2..c51094d 100644
--- a/internal/lsp/cache/analysis.go
+++ b/internal/lsp/cache/analysis.go
@@ -207,7 +207,7 @@
}
defer func() {
if r := recover(); r != nil {
- event.Log(ctx, fmt.Sprintf("analysis panicked: %s", r), tag.Package.Of(pkg.PkgPath()))
+ event.Log(ctx, fmt.Sprintf("analysis panicked: %s", r), tag.Package.Of(pkg.ID()))
data.err = errors.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r)
}
}()
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index cdcc3ae..d73a0d5 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -128,20 +128,17 @@
event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs)))
}
if len(pkgs) == 0 {
- if err != nil {
- // Try to extract the load error into a structured error with
- // diagnostics.
- if criticalErr := s.parseLoadError(ctx, err); criticalErr != nil {
- return criticalErr
- }
- } else {
+ if err == nil {
err = fmt.Errorf("no packages returned")
}
return errors.Errorf("%v: %w", err, source.PackagesLoadError)
}
for _, pkg := range pkgs {
if !containsDir || s.view.Options().VerboseOutput {
- event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.PackagePath.Of(pkg.PkgPath), tag.Files.Of(pkg.CompiledGoFiles))
+ event.Log(ctx, "go/packages.Load",
+ tag.Snapshot.Of(s.ID()),
+ tag.Package.Of(pkg.ID),
+ tag.Files.Of(pkg.CompiledGoFiles))
}
// Ignore packages with no sources, since we will never be able to
// correctly invalidate that metadata.
@@ -179,31 +176,9 @@
return nil
}
-func (s *snapshot) parseLoadError(ctx context.Context, loadErr error) *source.CriticalError {
- if strings.Contains(loadErr.Error(), "cannot find main module") {
- return s.WorkspaceLayoutError(ctx)
- }
- criticalErr := &source.CriticalError{
- MainError: loadErr,
- }
- // Attempt to place diagnostics in the relevant go.mod files, if any.
- for _, uri := range s.ModFiles() {
- fh, err := s.GetFile(ctx, uri)
- if err != nil {
- continue
- }
- srcErr := s.extractGoCommandError(ctx, s, fh, loadErr.Error())
- if srcErr == nil {
- continue
- }
- criticalErr.ErrorList = append(criticalErr.ErrorList, srcErr)
- }
- return criticalErr
-}
-
// workspaceLayoutErrors returns a diagnostic for every open file, as well as
// an error message if there are no open files.
-func (s *snapshot) WorkspaceLayoutError(ctx context.Context) *source.CriticalError {
+func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError {
if len(s.workspace.getKnownModFiles()) == 0 {
return nil
}
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index a5f0e1e..cfd3fd1 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -73,7 +73,7 @@
// Attempt to convert the error to a standardized parse error.
var parseErrors []*source.Error
if err != nil {
- if parseErr, extractErr := extractErrorWithPosition(ctx, err.Error(), s); extractErr == nil {
+ if parseErr := extractErrorWithPosition(ctx, err.Error(), s); parseErr != nil {
parseErrors = []*source.Error{parseErr}
}
}
@@ -341,29 +341,39 @@
// extractGoCommandError tries to parse errors that come from the go command
// and shape them into go.mod diagnostics.
-func (s *snapshot) extractGoCommandError(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, goCmdError string) *source.Error {
+func (s *snapshot) extractGoCommandErrors(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, goCmdError string) []*source.Error {
+ var srcErrs []*source.Error
+ if srcErr := s.parseModError(ctx, fh, goCmdError); srcErr != nil {
+ srcErrs = append(srcErrs, srcErr)
+ }
// If the error message contains a position, use that. Don't pass a file
// handle in, as it might not be the file associated with the error.
- if srcErr, err := extractErrorWithPosition(ctx, goCmdError, s); err == nil {
- return srcErr
+ if srcErr := extractErrorWithPosition(ctx, goCmdError, s); srcErr != nil {
+ srcErrs = append(srcErrs, srcErr)
+ } else if srcErr := s.matchErrorToModule(ctx, fh, goCmdError); srcErr != nil {
+ srcErrs = append(srcErrs, srcErr)
}
- // We try to match module versions in error messages. Some examples:
- //
- // example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory
- // 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.
+ return srcErrs
+}
+
+// matchErrorToModule attempts to match module version in error messages.
+// Some examples:
+//
+// example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory
+// 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.
+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 _, s := range fields {
- s = strings.TrimSpace(s)
- match := moduleAtVersionRe.FindStringSubmatch(s)
- if match == nil || len(match) < 3 {
+ for _, field := range fields {
+ match := moduleAtVersionRe.FindStringSubmatch(field)
+ if match == nil {
continue
}
path, version := match[1], match[2]
@@ -378,7 +388,7 @@
v.Path, v.Version = path, version
break
}
- pm, err := snapshot.ParseMod(ctx, fh)
+ pm, err := s.ParseMod(ctx, fh)
if err != nil {
return nil
}
@@ -387,13 +397,19 @@
if err != nil {
return nil
}
- if v.Path != "" && strings.Contains(goCmdError, "disabled by GOPROXY=off") {
+ 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: fmt.Sprintf("%v@%v has not been downloaded", v.Path, v.Version),
+ Message: msg,
Kind: source.ListError,
Range: rng,
URI: fh.URI(),
@@ -411,6 +427,7 @@
Message: goCmdError,
Range: rng,
URI: fh.URI(),
+ Kind: source.ListError,
}
}
// Check if there are any require, exclude, or replace statements that
@@ -449,10 +466,10 @@
// information for the given unstructured error. If a file handle is provided,
// the error position will be on that file. This is useful for parse errors,
// where we already know the file with the error.
-func extractErrorWithPosition(ctx context.Context, goCmdError string, src source.FileSource) (*source.Error, error) {
+func extractErrorWithPosition(ctx context.Context, goCmdError string, src source.FileSource) *source.Error {
matches := errorPositionRe.FindStringSubmatch(strings.TrimSpace(goCmdError))
if len(matches) == 0 {
- return nil, fmt.Errorf("error message doesn't contain a position")
+ return nil
}
var pos, msg string
for i, name := range errorPositionRe.SubexpNames() {
@@ -466,11 +483,11 @@
spn := span.Parse(pos)
fh, err := src.GetFile(ctx, spn.URI())
if err != nil {
- return nil, err
+ return nil
}
content, err := fh.Read()
if err != nil {
- return nil, err
+ return nil
}
m := &protocol.ColumnMapper{
URI: spn.URI(),
@@ -479,7 +496,7 @@
}
rng, err := m.Range(spn)
if err != nil {
- return nil, err
+ return nil
}
category := GoCommandError
if fh != nil {
@@ -490,5 +507,5 @@
Message: msg,
Range: rng,
URI: spn.URI(),
- }, nil
+ }
}
diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go
index 1dec13c..1650efa 100644
--- a/internal/lsp/cache/mod_tidy.go
+++ b/internal/lsp/cache/mod_tidy.go
@@ -71,11 +71,13 @@
return nil, source.ErrNoModOnDisk
}
}
+ if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
+ return &source.TidiedModule{
+ Errors: criticalErr.ErrorList,
+ }, nil
+ }
workspacePkgs, err := s.WorkspacePackages(ctx)
if err != nil {
- if tm, ok := s.parseModErrors(ctx, fh, err); ok {
- return tm, nil
- }
return nil, err
}
importHash, err := hashImports(ctx, workspacePkgs)
@@ -149,87 +151,77 @@
return mth.tidy(ctx, s)
}
-func (s *snapshot) parseModErrors(ctx context.Context, fh source.FileHandle, goCommandErr error) (*source.TidiedModule, bool) {
- if goCommandErr == nil {
- return nil, false
- }
-
+func (s *snapshot) parseModError(ctx context.Context, fh source.FileHandle, errText string) *source.Error {
// Match on common error messages. This is really hacky, but I'm not sure
// of any better way. This can be removed when golang/go#39164 is resolved.
- errText := goCommandErr.Error()
isInconsistentVendor := strings.Contains(errText, "inconsistent vendoring")
isGoSumUpdates := strings.Contains(errText, "updates to go.sum needed") || strings.Contains(errText, "missing go.sum entry")
if !isInconsistentVendor && !isGoSumUpdates {
- return nil, false
+ return nil
}
pmf, err := s.ParseMod(ctx, fh)
if err != nil {
- return nil, false
+ return nil
}
if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
- return nil, false
+ return nil
}
rng, err := rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
if err != nil {
- return nil, false
+ return nil
}
args, err := source.MarshalArgs(protocol.URIFromSpanURI(fh.URI()))
if err != nil {
- return nil, false
+ return nil
}
switch {
case isInconsistentVendor:
- return &source.TidiedModule{
- Errors: []*source.Error{{
- URI: fh.URI(),
- Range: rng,
- Kind: source.ListError,
- Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
+ return &source.Error{
+ URI: fh.URI(),
+ Range: rng,
+ Kind: source.ListError,
+ Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
- SuggestedFixes: []source.SuggestedFix{{
- Title: source.CommandVendor.Title,
- Command: &protocol.Command{
- Command: source.CommandVendor.ID(),
- Title: source.CommandVendor.Title,
- Arguments: args,
- },
- }},
- }},
- }, true
-
- case isGoSumUpdates:
- return &source.TidiedModule{
- Errors: []*source.Error{{
- URI: fh.URI(),
- Range: rng,
- Kind: source.ListError,
- Message: `go.sum is out of sync with go.mod. Please update it or run "go mod tidy".`,
- SuggestedFixes: []source.SuggestedFix{
- {
- Title: source.CommandTidy.Title,
- Command: &protocol.Command{
- Command: source.CommandTidy.ID(),
- Title: source.CommandTidy.Title,
- Arguments: args,
- },
- },
- {
- Title: source.CommandUpdateGoSum.Title,
- Command: &protocol.Command{
- Command: source.CommandUpdateGoSum.ID(),
- Title: source.CommandUpdateGoSum.Title,
- Arguments: args,
- },
- },
+ SuggestedFixes: []source.SuggestedFix{{
+ Title: source.CommandVendor.Title,
+ Command: &protocol.Command{
+ Command: source.CommandVendor.ID(),
+ Title: source.CommandVendor.Title,
+ Arguments: args,
},
}},
- }, true
- }
+ }
- return nil, false
+ case isGoSumUpdates:
+ return &source.Error{
+ URI: fh.URI(),
+ Range: rng,
+ Kind: source.ListError,
+ Message: `go.sum is out of sync with go.mod. Please update it or run "go mod tidy".`,
+ SuggestedFixes: []source.SuggestedFix{
+ {
+ Title: source.CommandTidy.Title,
+ Command: &protocol.Command{
+ Command: source.CommandTidy.ID(),
+ Title: source.CommandTidy.Title,
+ Arguments: args,
+ },
+ },
+ {
+ Title: source.CommandUpdateGoSum.Title,
+ Command: &protocol.Command{
+ Command: source.CommandUpdateGoSum.ID(),
+ Title: source.CommandUpdateGoSum.Title,
+ Arguments: args,
+ },
+ },
+ },
+ }
+ }
+ return nil
}
func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
@@ -274,13 +266,6 @@
}
delete(unused, req.Mod.Path)
}
- for _, req := range unused {
- srcErr, err := unusedError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
- if err != nil {
- return nil, err
- }
- errors = append(errors, srcErr)
- }
for _, req := range wrongDirectness {
// Handle dependencies that are incorrectly labeled indirect and
// vice versa.
@@ -380,16 +365,25 @@
}
}
}
+ // Finally, add errors for any unused dependencies.
+ onlyError := len(errors) == 0 && len(unused) == 1
+ for _, req := range unused {
+ srcErr, err := unusedError(pm.Mapper, req, onlyError, snapshot.View().Options().ComputeEdits)
+ if err != nil {
+ return nil, err
+ }
+ errors = append(errors, srcErr)
+ }
return errors, nil
}
// unusedError returns a source.Error for an unused require.
-func unusedError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Error, error) {
+func unusedError(m *protocol.ColumnMapper, req *modfile.Require, onlyError bool, computeEdits diff.ComputeEdits) (*source.Error, error) {
rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
if err != nil {
return nil, err
}
- args, err := source.MarshalArgs(m.URI, false, []string{req.Mod.Path + "@none"})
+ args, err := source.MarshalArgs(m.URI, onlyError, req.Mod.Path)
if err != nil {
return nil, err
}
@@ -511,7 +505,10 @@
return nil, err
}
// Calculate the edits to be made due to the change.
- diff := computeEdits(m.URI, string(m.Content), string(newContent))
+ diff, err := computeEdits(m.URI, string(m.Content), string(newContent))
+ if err != nil {
+ return nil, err
+ }
return source.ToProtocolEdits(m, diff)
}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index b06d9c8..08459a7 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -25,6 +25,7 @@
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
+ "golang.org/x/tools/internal/lsp/debug/log"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/memoize"
@@ -993,6 +994,84 @@
}
func (s *snapshot) awaitLoaded(ctx context.Context) error {
+ err := s.awaitLoadedAllErrors(ctx)
+
+ // If we still have absolutely no metadata, check if the view failed to
+ // initialize and return any errors.
+ // TODO(rstambler): Should we clear the error after we return it?
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if len(s.metadata) == 0 {
+ return err
+ }
+ return nil
+}
+
+func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError {
+ loadErr := s.awaitLoadedAllErrors(ctx)
+
+ // Even if packages didn't fail to load, we still may want to show
+ // additional warnings.
+ if loadErr == nil {
+ wsPkgs, _ := s.WorkspacePackages(ctx)
+ if msg := shouldShowAdHocPackagesWarning(s, wsPkgs); msg != "" {
+ return &source.CriticalError{
+ MainError: fmt.Errorf(msg),
+ }
+ }
+ // Even if workspace packages were returned, there still may be an error
+ // with the user's workspace layout. Workspace packages that only have the
+ // ID "command-line-arguments" are usually a symptom of a bad workspace
+ // configuration.
+ if containsCommandLineArguments(wsPkgs) {
+ return s.workspaceLayoutError(ctx)
+ }
+ return nil
+ }
+
+ if strings.Contains(loadErr.Error(), "cannot find main module") {
+ return s.workspaceLayoutError(ctx)
+ }
+ criticalErr := &source.CriticalError{
+ MainError: loadErr,
+ }
+ // Attempt to place diagnostics in the relevant go.mod files, if any.
+ for _, uri := range s.ModFiles() {
+ fh, err := s.GetFile(ctx, uri)
+ if err != nil {
+ continue
+ }
+ criticalErr.ErrorList = append(criticalErr.ErrorList, s.extractGoCommandErrors(ctx, s, fh, loadErr.Error())...)
+ }
+ return criticalErr
+}
+
+const adHocPackagesWarning = `You are outside of a module and outside of $GOPATH/src.
+If you are using modules, please open your editor to a directory in your module.
+If you believe this warning is incorrect, please file an issue: https://github.com/golang/go/issues/new.`
+
+func shouldShowAdHocPackagesWarning(snapshot source.Snapshot, pkgs []source.Package) string {
+ if snapshot.ValidBuildConfiguration() {
+ return ""
+ }
+ for _, pkg := range pkgs {
+ if len(pkg.MissingDependencies()) > 0 {
+ return adHocPackagesWarning
+ }
+ }
+ return ""
+}
+
+func containsCommandLineArguments(pkgs []source.Package) bool {
+ for _, pkg := range pkgs {
+ if strings.Contains(pkg.ID(), "command-line-arguments") {
+ return true
+ }
+ }
+ return false
+}
+
+func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) error {
// Do not return results until the snapshot's view has been initialized.
s.AwaitInitialized(ctx)
@@ -1002,15 +1081,10 @@
if err := s.reloadOrphanedFiles(ctx); err != nil {
return err
}
- // If we still have absolutely no metadata, check if the view failed to
- // initialize and return any errors.
- // TODO(rstambler): Should we clear the error after we return it?
- s.mu.Lock()
- defer s.mu.Unlock()
- if len(s.metadata) == 0 {
- return s.initializedErr
- }
- return nil
+ // TODO(rstambler): Should we be more careful about returning the
+ // initialization error? Is it possible for the initialization error to be
+ // corrected without a successful reinitialization?
+ return s.initializedErr
}
func (s *snapshot) AwaitInitialized(ctx context.Context) {
@@ -1158,17 +1232,38 @@
return fmt.Sprintf("v%v/%v", v.id, snapshotID)
}
+// checkSnapshotLocked verifies that some invariants are preserved on the
+// snapshot.
+func checkSnapshotLocked(ctx context.Context, s *snapshot) {
+ // Check that every go file for a workspace package is identified as
+ // belonging to that workspace package.
+ for wsID := range s.workspacePackages {
+ if m, ok := s.metadata[wsID]; ok {
+ for _, uri := range m.goFiles {
+ found := false
+ for _, id := range s.ids[uri] {
+ if id == wsID {
+ found = true
+ break
+ }
+ }
+ if !found {
+ log.Error.Logf(ctx, "workspace package %v not associated with %v", wsID, uri)
+ }
+ }
+ }
+ }
+}
+
func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) (*snapshot, bool) {
- // Track some important types of changes.
- var (
- vendorChanged bool
- modulesChanged bool
- )
- newWorkspace, workspaceChanged := s.workspace.invalidate(ctx, changes)
+ var vendorChanged bool
+ newWorkspace, workspaceChanged, workspaceReload := s.workspace.invalidate(ctx, changes)
s.mu.Lock()
defer s.mu.Unlock()
+ checkSnapshotLocked(ctx, s)
+
newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1))
bgCtx, cancel := context.WithCancel(bgCtx)
result := &snapshot{
@@ -1253,7 +1348,7 @@
// It maps id->invalidateMetadata.
directIDs := map[packageID]bool{}
// Invalidate all package metadata if the workspace module has changed.
- if workspaceChanged {
+ if workspaceReload {
for k := range s.metadata {
directIDs[k] = true
}
@@ -1273,7 +1368,7 @@
// Check if the file's package name or imports have changed,
// and if so, invalidate this file's packages' metadata.
shouldInvalidateMetadata, pkgNameChanged := s.shouldInvalidateMetadata(ctx, result, originalFH, change.fileHandle)
- invalidateMetadata := forceReloadMetadata || shouldInvalidateMetadata
+ invalidateMetadata := forceReloadMetadata || workspaceReload || shouldInvalidateMetadata
// Mark all of the package IDs containing the given file.
// TODO: if the file has moved into a new package, we should invalidate that too.
@@ -1306,9 +1401,6 @@
// If the view's go.mod file's contents have changed, invalidate
// the metadata for every known package in the snapshot.
delete(result.parseModHandles, uri)
- if _, ok := result.workspace.getActiveModFiles()[uri]; ok {
- modulesChanged = true
- }
}
// Handle the invalidated file; it may have new contents or not exist.
if !change.exists {
@@ -1316,6 +1408,7 @@
} else {
result.files[uri] = change.fileHandle
}
+
// Make sure to remove the changed file from the unloadable set.
delete(result.unloadableFiles, uri)
}
@@ -1447,7 +1540,7 @@
}
// The snapshot may need to be reinitialized.
- if modulesChanged || workspaceChanged || vendorChanged {
+ if workspaceReload || vendorChanged {
if workspaceChanged || result.initializedErr != nil {
result.initializeOnce = &sync.Once{}
}
@@ -1532,13 +1625,6 @@
if originalFH.FileIdentity() == currentFH.FileIdentity() {
return false, false
}
- // If a go.mod in the workspace has been changed, invalidate metadata.
- if kind := originalFH.Kind(); kind == source.Mod {
- if !source.InDir(filepath.Dir(s.view.rootURI.Filename()), originalFH.URI().Filename()) {
- return false, false
- }
- return currentFH.Saved(), false
- }
// Get the original and current parsed files in order to check package name
// and imports. Use the new snapshot to parse to avoid modifying the
// current snapshot.
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 384ef98..16bef0b 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -580,6 +580,10 @@
} else {
s.initializedErr = err
}
+ } else {
+ // Clear out the initialization error, in case it had been set
+ // previously.
+ s.initializedErr = nil
}
})
}
diff --git a/internal/lsp/cache/view_test.go b/internal/lsp/cache/view_test.go
index 054d410..fb788f4 100644
--- a/internal/lsp/cache/view_test.go
+++ b/internal/lsp/cache/view_test.go
@@ -96,7 +96,7 @@
rel := fake.RelativeTo(dir)
folderURI := span.URIFromPath(rel.AbsPath(test.folder))
excludeNothing := func(string) bool { return false }
- got, err := findWorkspaceRoot(ctx, folderURI, osFileSource{}, excludeNothing, test.experimental)
+ got, err := findWorkspaceRoot(ctx, folderURI, &osFileSource{}, excludeNothing, test.experimental)
if err != nil {
t.Fatal(err)
}
@@ -159,7 +159,7 @@
}
// Try directly parsing the given, invalid go.mod file. Then, extract a
// position from the error message.
- src := osFileSource{}
+ src := &osFileSource{}
modFH, err := src.GetFile(ctx, uri)
if err != nil {
t.Fatal(err)
@@ -172,9 +172,9 @@
if parseErr == nil {
t.Fatalf("%s: expected an unparseable go.mod file", uri.Filename())
}
- srcErr, err := extractErrorWithPosition(ctx, parseErr.Error(), src)
- if err != nil {
- t.Fatal(err)
+ srcErr := extractErrorWithPosition(ctx, parseErr.Error(), src)
+ if srcErr == nil {
+ t.Fatalf("unable to extract positions from %v", parseErr.Error())
}
if srcErr.URI != uri {
t.Errorf("unexpected URI: got %s, wanted %s", srcErr.URI, uri)
diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go
index 217d31f..84db093 100644
--- a/internal/lsp/cache/workspace.go
+++ b/internal/lsp/cache/workspace.go
@@ -238,119 +238,134 @@
return dirs
}
-// invalidate returns a (possibly) new workspaceModule after invalidating
-// changedURIs. If wm is still valid in the presence of changedURIs, it returns
-// itself unmodified.
-func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange) (*workspace, bool) {
- // Prevent races to wm.modFile or wm.wsDirs below, if wm has not yet been
- // built.
+// invalidate returns a (possibly) new workspace after invalidating the changed
+// files. If w is still valid in the presence of changedURIs, it returns itself
+// unmodified.
+//
+// The returned changed and reload flags control the level of invalidation.
+// Some workspace changes may affect workspace contents without requiring a
+// reload of metadata (for example, unsaved changes to a go.mod or go.sum
+// file).
+func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange) (_ *workspace, changed, reload bool) {
+ // Prevent races to w.modFile or w.wsDirs below, if wmhas not yet been built.
w.buildMu.Lock()
defer w.buildMu.Unlock()
- // Any gopls.mod change is processed first, followed by go.mod changes, as
- // changes to gopls.mod may affect the set of active go.mod files.
- var (
- // New values. We return a new workspace module if and only if
- // knownModFiles is non-nil.
- knownModFiles map[span.URI]struct{}
- moduleSource = w.moduleSource
- modFile = w.mod
- sumData = w.sum
- err error
- )
- if w.moduleSource == goplsModWorkspace {
- // If we are currently reading the modfile from gopls.mod, we default to
- // preserving it even if module metadata changes (which may be the case if
- // a go.sum file changes).
- modFile = w.mod
+ // Clone the workspace. This may be discarded if nothing changed.
+ result := &workspace{
+ root: w.root,
+ moduleSource: w.moduleSource,
+ knownModFiles: make(map[span.URI]struct{}),
+ activeModFiles: make(map[span.URI]struct{}),
+ go111moduleOff: w.go111moduleOff,
+ mod: w.mod,
+ sum: w.sum,
+ wsDirs: w.wsDirs,
}
- // First handle changes to the gopls.mod file.
+ for k, v := range w.knownModFiles {
+ result.knownModFiles[k] = v
+ }
+ for k, v := range w.activeModFiles {
+ result.activeModFiles[k] = v
+ }
+
+ // First handle changes to the gopls.mod file. This must be considered before
+ // any changes to go.mod or go.sum files, as the gopls.mod file determines
+ // which modules we care about. In legacy workspace mode we don't consider
+ // the gopls.mod file.
if w.moduleSource != legacyWorkspace {
// If gopls.mod has changed we need to either re-read it if it exists or
- // walk the filesystem if it doesn't exist.
+ // walk the filesystem if it has been deleted.
gmURI := goplsModURI(w.root)
if change, ok := changes[gmURI]; ok {
if change.exists {
- // Only invalidate if the gopls.mod actually parses. Otherwise, stick with the current gopls.mod
+ // Only invalidate if the gopls.mod actually parses.
+ // Otherwise, stick with the current gopls.mod.
parsedFile, parsedModules, err := parseGoplsMod(w.root, gmURI, change.content)
if err == nil {
- modFile = parsedFile
- moduleSource = goplsModWorkspace
- knownModFiles = parsedModules
+ changed = true
+ reload = change.fileHandle.Saved()
+ result.mod = parsedFile
+ result.moduleSource = goplsModWorkspace
+ result.knownModFiles = parsedModules
+ result.activeModFiles = make(map[span.URI]struct{})
+ for k, v := range parsedModules {
+ result.activeModFiles[k] = v
+ }
} else {
- // Note that modFile is not invalidated here.
+ // An unparseable gopls.mod file should not invalidate the
+ // workspace: nothing good could come from changing the
+ // workspace in this case.
event.Error(ctx, "parsing gopls.mod", err)
}
} else {
// gopls.mod is deleted. search for modules again.
- moduleSource = fileSystemWorkspace
- knownModFiles, err = findModules(ctx, w.root, w.excludePath, 0)
- // the modFile is no longer valid.
+ changed = true
+ reload = true
+ result.moduleSource = fileSystemWorkspace
+ // The parsed gopls.mod is no longer valid.
+ result.mod = nil
+ knownModFiles, err := findModules(ctx, w.root, w.excludePath, 0)
if err != nil {
+ result.knownModFiles = nil
+ result.activeModFiles = nil
event.Error(ctx, "finding file system modules", err)
+ } else {
+ result.knownModFiles = knownModFiles
+ result.activeModFiles = make(map[span.URI]struct{})
+ for k, v := range result.knownModFiles {
+ result.activeModFiles[k] = v
+ }
}
- modFile = nil
}
}
}
- // Next, handle go.mod changes that could affect our set of tracked modules.
- // If we're reading our tracked modules from the gopls.mod, there's nothing
- // to do here.
- if w.moduleSource != goplsModWorkspace {
+ // Next, handle go.mod changes that could affect our workspace. If we're
+ // reading our tracked modules from the gopls.mod, there's nothing to do
+ // here.
+ if result.moduleSource != goplsModWorkspace {
for uri, change := range changes {
- // If a go.mod file has changed, we may need to update the set of active
- // modules.
- if !isGoMod(uri) {
+ if !isGoMod(uri) || !source.InDir(result.root.Filename(), uri.Filename()) {
continue
}
- if !source.InDir(w.root.Filename(), uri.Filename()) {
- // Otherwise, the module must be contained within the workspace root.
- continue
- }
- if knownModFiles == nil {
- knownModFiles = make(map[span.URI]struct{})
- for k := range w.knownModFiles {
- knownModFiles[k] = struct{}{}
- }
- }
+ changed = true
+ active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0
+ reload = reload || (active && change.fileHandle.Saved())
if change.exists {
- knownModFiles[uri] = struct{}{}
- } else {
- delete(knownModFiles, uri)
- }
- }
- }
- if knownModFiles != nil {
- var activeModFiles map[span.URI]struct{}
- if w.go111moduleOff {
- // If GO111MODULE=off, the set of active go.mod files is unchanged.
- activeModFiles = w.activeModFiles
- } else {
- activeModFiles = make(map[span.URI]struct{})
- for uri := range knownModFiles {
- // Legacy mode only considers a module a workspace root, so don't
- // update the active go.mod files map.
- if w.moduleSource == legacyWorkspace && source.CompareURI(modURI(w.root), uri) != 0 {
- continue
+ result.knownModFiles[uri] = struct{}{}
+ if active {
+ result.activeModFiles[uri] = struct{}{}
}
- activeModFiles[uri] = struct{}{}
+ } else {
+ delete(result.knownModFiles, uri)
+ delete(result.activeModFiles, uri)
}
}
- // Any change to modules triggers a new version.
- return &workspace{
- root: w.root,
- excludePath: w.excludePath,
- moduleSource: moduleSource,
- activeModFiles: activeModFiles,
- knownModFiles: knownModFiles,
- mod: modFile,
- sum: sumData,
- wsDirs: w.wsDirs,
- }, true
}
- // No change. Just return wm, since it is immutable.
- return w, false
+
+ // Finally, process go.sum changes for any modules that are now active.
+ for uri, change := range changes {
+ if !isGoSum(uri) {
+ continue
+ }
+ // TODO(rFindley) factor out this URI mangling.
+ dir := filepath.Dir(uri.Filename())
+ modURI := span.URIFromPath(filepath.Join(dir, "go.mod"))
+ if _, active := result.activeModFiles[modURI]; !active {
+ continue
+ }
+ // Only changes to active go.sum files actually cause the workspace to
+ // change.
+ changed = true
+ reload = reload || change.fileHandle.Saved()
+ }
+
+ if !changed {
+ return w, false, false
+ }
+
+ return result, changed, reload
}
// goplsModURI returns the URI for the gopls.mod file contained in root.
@@ -368,6 +383,10 @@
return filepath.Base(uri.Filename()) == "go.mod"
}
+func isGoSum(uri span.URI) bool {
+ return filepath.Base(uri.Filename()) == "go.sum"
+}
+
// fileExists reports if the file uri exists within source.
func fileExists(ctx context.Context, uri span.URI, source source.FileSource) (bool, error) {
fh, err := source.GetFile(ctx, uri)
diff --git a/internal/lsp/cache/workspace_test.go b/internal/lsp/cache/workspace_test.go
index c0d36a7..910e705 100644
--- a/internal/lsp/cache/workspace_test.go
+++ b/internal/lsp/cache/workspace_test.go
@@ -7,6 +7,7 @@
import (
"context"
"os"
+ "strings"
"testing"
"golang.org/x/tools/internal/lsp/fake"
@@ -15,9 +16,64 @@
)
// osFileSource is a fileSource that just reads from the operating system.
-type osFileSource struct{}
+type osFileSource struct {
+ overlays map[span.URI]fakeOverlay
+}
-func (s osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
+type fakeOverlay struct {
+ source.VersionedFileHandle
+ uri span.URI
+ content string
+ err error
+ saved bool
+}
+
+func (o fakeOverlay) Saved() bool { return o.saved }
+
+func (o fakeOverlay) Read() ([]byte, error) {
+ if o.err != nil {
+ return nil, o.err
+ }
+ return []byte(o.content), nil
+}
+
+func (o fakeOverlay) URI() span.URI {
+ return o.uri
+}
+
+// change updates the file source with the given file content. For convenience,
+// empty content signals a deletion. If saved is true, these changes are
+// persisted to disk.
+func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) {
+ if content == "" {
+ delete(s.overlays, uri)
+ if saved {
+ if err := os.Remove(uri.Filename()); err != nil {
+ return nil, err
+ }
+ }
+ fh, err := s.GetFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ data, err := fh.Read()
+ return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil
+ }
+ if s.overlays == nil {
+ s.overlays = map[span.URI]fakeOverlay{}
+ }
+ s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved}
+ return &fileChange{
+ exists: content != "",
+ content: []byte(content),
+ fileHandle: s.overlays[uri],
+ }, nil
+}
+
+func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
+ if overlay, ok := s.overlays[uri]; ok {
+ return overlay, nil
+ }
fi, statErr := os.Stat(uri.Filename())
if statErr != nil {
return &fileHandle{
@@ -32,20 +88,28 @@
return fh, nil
}
+type wsState struct {
+ source workspaceSource
+ modules []string
+ dirs []string
+ sum string
+}
+
+type wsChange struct {
+ content string
+ saved bool
+}
+
func TestWorkspaceModule(t *testing.T) {
tests := []struct {
- desc string
- initial string // txtar-encoded
- legacyMode bool
- initialSource workspaceSource
- initialModules []string
- initialDirs []string
- initialSum string
- updates map[string]string
- finalSource workspaceSource
- finalModules []string
- finalDirs []string
- finalSum string
+ desc string
+ initial string // txtar-encoded
+ legacyMode bool
+ initialState wsState
+ updates map[string]wsChange
+ wantChanged bool
+ wantReload bool
+ finalState wsState
}{
{
desc: "legacy mode",
@@ -56,11 +120,13 @@
golang.org/x/mod v0.3.0 h1:deadbeef
-- a/go.mod --
module moda.com`,
- legacyMode: true,
- initialModules: []string{"./go.mod"},
- initialSource: legacyWorkspace,
- initialDirs: []string{"."},
- initialSum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
+ legacyMode: true,
+ initialState: wsState{
+ modules: []string{"./go.mod"},
+ source: legacyWorkspace,
+ dirs: []string{"."},
+ sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
+ },
},
{
desc: "nested module",
@@ -69,9 +135,11 @@
module mod.com
-- a/go.mod --
module moda.com`,
- initialModules: []string{"./go.mod", "a/go.mod"},
- initialSource: fileSystemWorkspace,
- initialDirs: []string{".", "a"},
+ initialState: wsState{
+ modules: []string{"./go.mod", "a/go.mod"},
+ source: fileSystemWorkspace,
+ dirs: []string{".", "a"},
+ },
},
{
desc: "removing module",
@@ -84,20 +152,26 @@
module modb.com
-- b/go.sum --
golang.org/x/mod v0.3.0 h1:beefdead`,
- initialModules: []string{"a/go.mod", "b/go.mod"},
- initialSource: fileSystemWorkspace,
- initialDirs: []string{".", "a", "b"},
- initialSum: "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n",
- updates: map[string]string{
- "gopls.mod": `module gopls-workspace
+ initialState: wsState{
+ modules: []string{"a/go.mod", "b/go.mod"},
+ source: fileSystemWorkspace,
+ dirs: []string{".", "a", "b"},
+ sum: "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n",
+ },
+ updates: map[string]wsChange{
+ "gopls.mod": {`module gopls-workspace
require moda.com v0.0.0-goplsworkspace
-replace moda.com => $SANDBOX_WORKDIR/a`,
+replace moda.com => $SANDBOX_WORKDIR/a`, true},
},
- finalModules: []string{"a/go.mod"},
- finalSource: goplsModWorkspace,
- finalDirs: []string{".", "a"},
- finalSum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
+ wantChanged: true,
+ wantReload: true,
+ finalState: wsState{
+ modules: []string{"a/go.mod"},
+ source: goplsModWorkspace,
+ dirs: []string{".", "a"},
+ sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
+ },
},
{
desc: "adding module",
@@ -109,21 +183,27 @@
module moda.com
-- b/go.mod --
module modb.com`,
- initialModules: []string{"a/go.mod"},
- initialSource: goplsModWorkspace,
- initialDirs: []string{".", "a"},
- updates: map[string]string{
- "gopls.mod": `module gopls-workspace
+ initialState: wsState{
+ modules: []string{"a/go.mod"},
+ source: goplsModWorkspace,
+ dirs: []string{".", "a"},
+ },
+ updates: map[string]wsChange{
+ "gopls.mod": {`module gopls-workspace
require moda.com v0.0.0-goplsworkspace
require modb.com v0.0.0-goplsworkspace
replace moda.com => $SANDBOX_WORKDIR/a
-replace modb.com => $SANDBOX_WORKDIR/b`,
+replace modb.com => $SANDBOX_WORKDIR/b`, true},
},
- finalModules: []string{"a/go.mod", "b/go.mod"},
- finalSource: goplsModWorkspace,
- finalDirs: []string{".", "a", "b"},
+ wantChanged: true,
+ wantReload: true,
+ finalState: wsState{
+ modules: []string{"a/go.mod", "b/go.mod"},
+ source: goplsModWorkspace,
+ dirs: []string{".", "a", "b"},
+ },
},
{
desc: "deleting gopls.mod",
@@ -137,15 +217,21 @@
module moda.com
-- b/go.mod --
module modb.com`,
- initialModules: []string{"a/go.mod"},
- initialSource: goplsModWorkspace,
- initialDirs: []string{".", "a"},
- updates: map[string]string{
- "gopls.mod": "",
+ initialState: wsState{
+ modules: []string{"a/go.mod"},
+ source: goplsModWorkspace,
+ dirs: []string{".", "a"},
},
- finalModules: []string{"a/go.mod", "b/go.mod"},
- finalSource: fileSystemWorkspace,
- finalDirs: []string{".", "a", "b"},
+ updates: map[string]wsChange{
+ "gopls.mod": {"", true},
+ },
+ wantChanged: true,
+ wantReload: true,
+ finalState: wsState{
+ modules: []string{"a/go.mod", "b/go.mod"},
+ source: fileSystemWorkspace,
+ dirs: []string{".", "a", "b"},
+ },
},
{
desc: "broken module parsing",
@@ -157,20 +243,26 @@
replace gopls.test => ../../gopls.test // (this path shouldn't matter)
-- b/go.mod --
module modb.com`,
- initialModules: []string{"a/go.mod", "b/go.mod"},
- initialSource: fileSystemWorkspace,
- initialDirs: []string{".", "a", "b", "../gopls.test"},
- updates: map[string]string{
- "a/go.mod": `modul moda.com
+ initialState: wsState{
+ modules: []string{"a/go.mod", "b/go.mod"},
+ source: fileSystemWorkspace,
+ dirs: []string{".", "a", "b", "../gopls.test"},
+ },
+ updates: map[string]wsChange{
+ "a/go.mod": {`modul moda.com
require gopls.test v0.0.0-goplsworkspace
-replace gopls.test => ../../gopls.test2`,
+replace gopls.test => ../../gopls.test2`, false},
},
- finalModules: []string{"a/go.mod", "b/go.mod"},
- finalSource: fileSystemWorkspace,
- // finalDirs should be unchanged: we should preserve dirs in the presence
- // of a broken modfile.
- finalDirs: []string{".", "a", "b", "../gopls.test"},
+ wantChanged: true,
+ wantReload: false,
+ finalState: wsState{
+ modules: []string{"a/go.mod", "b/go.mod"},
+ source: fileSystemWorkspace,
+ // finalDirs should be unchanged: we should preserve dirs in the presence
+ // of a broken modfile.
+ dirs: []string{".", "a", "b", "../gopls.test"},
+ },
},
}
@@ -184,80 +276,49 @@
defer os.RemoveAll(dir)
root := span.URIFromPath(dir)
- fs := osFileSource{}
+ fs := &osFileSource{}
excludeNothing := func(string) bool { return false }
w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode)
if err != nil {
t.Fatal(err)
}
rel := fake.RelativeTo(dir)
- checkWorkspaceModule(t, rel, w, test.initialSource, test.initialModules)
- gotDirs := w.dirs(ctx, fs)
- checkWorkspaceDirs(t, rel, gotDirs, test.initialDirs)
-
- // Verify the initial sum.
- gotSumBytes, err := w.sumFile(ctx, fs)
- if err != nil {
- t.Fatal(err)
- }
- if gotSum := string(gotSumBytes); gotSum != test.initialSum {
- t.Errorf("got initial sum %q, want %q", gotSum, test.initialSum)
- }
+ checkState(ctx, t, fs, rel, w, test.initialState)
// Apply updates.
if test.updates != nil {
changes := make(map[span.URI]*fileChange)
for k, v := range test.updates {
- if v == "" {
- // for convenience, use this to signal a deletion. TODO: more doc
- err := os.Remove(rel.AbsPath(k))
- if err != nil {
- t.Fatal(err)
- }
- } else {
- fake.WriteFileData(k, []byte(v), rel)
- }
+ content := strings.ReplaceAll(v.content, "$SANDBOX_WORKDIR", string(rel))
uri := span.URIFromPath(rel.AbsPath(k))
- fh, err := fs.GetFile(ctx, uri)
+ changes[uri], err = fs.change(ctx, uri, content, v.saved)
if err != nil {
t.Fatal(err)
}
- content, err := fh.Read()
- changes[uri] = &fileChange{
- content: content,
- exists: err == nil,
- fileHandle: &closedFile{fh},
- }
}
- w, _ := w.invalidate(ctx, changes)
- checkWorkspaceModule(t, rel, w, test.finalSource, test.finalModules)
- gotDirs := w.dirs(ctx, fs)
- checkWorkspaceDirs(t, rel, gotDirs, test.finalDirs)
-
- // Verify that the final sumfile reflects any changes (for example,
- // that modules may have gone out of scope).
- gotSumBytes, err := w.sumFile(ctx, fs)
- if err != nil {
- t.Fatal(err)
+ got, gotChanged, gotReload := w.invalidate(ctx, changes)
+ if gotChanged != test.wantChanged {
+ t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged)
}
- if gotSum := string(gotSumBytes); gotSum != test.finalSum {
- t.Errorf("got final sum %q, want %q", gotSum, test.finalSum)
+ if gotReload != test.wantReload {
+ t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload)
}
+ checkState(ctx, t, fs, rel, got, test.finalState)
}
})
}
}
-func checkWorkspaceModule(t *testing.T, rel fake.RelativeTo, got *workspace, wantSource workspaceSource, want []string) {
+func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fake.RelativeTo, got *workspace, want wsState) {
t.Helper()
- if got.moduleSource != wantSource {
- t.Errorf("module source = %v, want %v", got.moduleSource, wantSource)
+ if got.moduleSource != want.source {
+ t.Errorf("module source = %v, want %v", got.moduleSource, want.source)
}
modules := make(map[span.URI]struct{})
for k := range got.getActiveModFiles() {
modules[k] = struct{}{}
}
- for _, modPath := range want {
+ for _, modPath := range want.modules {
path := rel.AbsPath(modPath)
uri := span.URIFromPath(path)
if _, ok := modules[uri]; !ok {
@@ -268,15 +329,12 @@
for remaining := range modules {
t.Errorf("unexpected module %q", remaining)
}
-}
-
-func checkWorkspaceDirs(t *testing.T, rel fake.RelativeTo, got []span.URI, want []string) {
- t.Helper()
+ gotDirs := got.dirs(ctx, fs)
gotM := make(map[span.URI]bool)
- for _, dir := range got {
+ for _, dir := range gotDirs {
gotM[dir] = true
}
- for _, dir := range want {
+ for _, dir := range want.dirs {
path := rel.AbsPath(dir)
uri := span.URIFromPath(path)
if !gotM[uri] {
@@ -287,4 +345,11 @@
for remaining := range gotM {
t.Errorf("unexpected dir %q", remaining)
}
+ gotSumBytes, err := got.sumFile(ctx, fs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gotSum := string(gotSumBytes); gotSum != want.sum {
+ t.Errorf("got final sum %q, want %q", gotSum, want.sum)
+ }
}
diff --git a/internal/lsp/cmd/rename.go b/internal/lsp/cmd/rename.go
index 57cf846..5742082 100644
--- a/internal/lsp/cmd/rename.go
+++ b/internal/lsp/cmd/rename.go
@@ -31,15 +31,15 @@
}
func (r *rename) Name() string { return "rename" }
-func (r *rename) Usage() string { return "<position>" }
+func (r *rename) Usage() string { return "<position> <new name>" }
func (r *rename) ShortHelp() string { return "rename selected identifier" }
func (r *rename) DetailedHelp(f *flag.FlagSet) {
fmt.Fprint(f.Output(), `
Example:
$ # 1-based location (:line:column or :#position) of the thing to change
- $ gopls rename helper/helper.go:8:6
- $ gopls rename helper/helper.go:#53
+ $ gopls rename helper/helper.go:8:6 Foo
+ $ gopls rename helper/helper.go:#53 Foo
gopls rename flags are:
`)
diff --git a/internal/lsp/cmd/test/definition.go b/internal/lsp/cmd/test/definition.go
index 62ff22f..c82d9a6 100644
--- a/internal/lsp/cmd/test/definition.go
+++ b/internal/lsp/cmd/test/definition.go
@@ -51,7 +51,10 @@
return []byte(got), nil
})))
if expect != "" && !strings.HasPrefix(got, expect) {
- d := myers.ComputeEdits("", expect, got)
+ d, err := myers.ComputeEdits("", expect, got)
+ if err != nil {
+ t.Fatal(err)
+ }
t.Errorf("definition %v failed with %#v\n%s", tag, args, diff.ToUnified("expect", "got", expect, d))
}
}
diff --git a/internal/lsp/cmd/test/imports.go b/internal/lsp/cmd/test/imports.go
index 56cfda0..ce8aee5 100644
--- a/internal/lsp/cmd/test/imports.go
+++ b/internal/lsp/cmd/test/imports.go
@@ -20,7 +20,10 @@
return []byte(got), nil
}))
if want != got {
- d := myers.ComputeEdits(uri, want, got)
+ d, err := myers.ComputeEdits(uri, want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
t.Errorf("imports failed for %s, expected:\n%s", filename, diff.ToUnified("want", "got", want, d))
}
}
diff --git a/internal/lsp/cmd/test/suggested_fix.go b/internal/lsp/cmd/test/suggested_fix.go
index bf7fb30..160dcdf 100644
--- a/internal/lsp/cmd/test/suggested_fix.go
+++ b/internal/lsp/cmd/test/suggested_fix.go
@@ -30,6 +30,6 @@
return []byte(got), nil
}))
if want != got {
- t.Errorf("suggested fixes failed for %s:\n%s", filename, tests.Diff(want, got))
+ t.Errorf("suggested fixes failed for %s:\n%s", filename, tests.Diff(t, want, got))
}
}
diff --git a/internal/lsp/cmd/test/workspace_symbol.go b/internal/lsp/cmd/test/workspace_symbol.go
index f52ee11..ce965f0 100644
--- a/internal/lsp/cmd/test/workspace_symbol.go
+++ b/internal/lsp/cmd/test/workspace_symbol.go
@@ -48,6 +48,6 @@
}))
if expect != got {
- t.Errorf("workspace_symbol failed for %s:\n%s", query, tests.Diff(expect, got))
+ t.Errorf("workspace_symbol failed for %s:\n%s", query, tests.Diff(t, expect, got))
}
}
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 2c0be24..79c8663 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -14,6 +14,7 @@
"path/filepath"
"strings"
+ "golang.org/x/mod/modfile"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/cache"
@@ -216,7 +217,7 @@
return err
}
return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"})
- case source.CommandAddDependency, source.CommandUpgradeDependency, source.CommandRemoveDependency:
+ case source.CommandAddDependency, source.CommandUpgradeDependency:
var uri protocol.DocumentURI
var goCmdArgs []string
var addRequire bool
@@ -229,6 +230,56 @@
return err
}
return s.runGoGetModule(ctx, snapshot, uri.SpanURI(), addRequire, goCmdArgs)
+ case source.CommandRemoveDependency:
+ var uri protocol.DocumentURI
+ var modulePath string
+ var onlyError bool
+ if err := source.UnmarshalArgs(args, &uri, &onlyError, &modulePath); err != nil {
+ return err
+ }
+ snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
+ defer release()
+ if !ok {
+ return err
+ }
+ // If the module is tidied apart from the one unused diagnostic, we can
+ // run `go get module@none`, and then run `go mod tidy`. Otherwise, we
+ // must make textual edits.
+ // TODO(rstambler): In Go 1.17+, we will be able to use the go command
+ // without checking if the module is tidy.
+ if onlyError {
+ if err := s.runGoGetModule(ctx, snapshot, uri.SpanURI(), false, []string{modulePath + "@none"}); err != nil {
+ return err
+ }
+ return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"tidy"})
+ }
+ pm, err := snapshot.ParseMod(ctx, fh)
+ if err != nil {
+ return err
+ }
+ edits, err := dropDependency(snapshot, pm, modulePath)
+ if err != nil {
+ return err
+ }
+ response, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: []protocol.TextDocumentEdit{{
+ TextDocument: protocol.VersionedTextDocumentIdentifier{
+ Version: fh.Version(),
+ TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+ URI: protocol.URIFromSpanURI(fh.URI()),
+ },
+ },
+ Edits: edits,
+ }},
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if !response.Applied {
+ return fmt.Errorf("edits not applied because of %s", response.FailureReason)
+ }
case source.CommandGoGetPackage:
var uri protocol.DocumentURI
var pkg string
@@ -303,6 +354,31 @@
return nil
}
+// dropDependency returns the edits to remove the given require from the go.mod
+// file.
+func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePath string) ([]protocol.TextEdit, error) {
+ // We need a private copy of the parsed go.mod file, since we're going to
+ // modify it.
+ copied, err := modfile.Parse("", pm.Mapper.Content, nil)
+ if err != nil {
+ return nil, err
+ }
+ if err := copied.DropRequire(modulePath); err != nil {
+ return nil, err
+ }
+ copied.Cleanup()
+ newContent, err := copied.Format()
+ if err != nil {
+ return nil, err
+ }
+ // Calculate the edits to be made due to the change.
+ diff, err := snapshot.View().Options().ComputeEdits(pm.URI, string(pm.Mapper.Content), string(newContent))
+ if err != nil {
+ return nil, err
+ }
+ return source.ToProtocolEdits(pm.Mapper, diff)
+}
+
func (s *Server) runTests(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI, work *workDone, tests, benchmarks []string) error {
pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace)
if err != nil {
diff --git a/internal/lsp/debug/rpc.go b/internal/lsp/debug/rpc.go
index 2732971..033ee37 100644
--- a/internal/lsp/debug/rpc.go
+++ b/internal/lsp/debug/rpc.go
@@ -20,7 +20,7 @@
"golang.org/x/tools/internal/lsp/debug/tag"
)
-var rpcTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var RPCTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}RPC Information{{end}}
{{define "body"}}
<H2>Inbound</H2>
@@ -44,7 +44,7 @@
{{end}}
`))
-type rpcs struct {
+type Rpcs struct { // exported for testing
mu sync.Mutex
Inbound []*rpcStats // stats for incoming lsp rpcs sorted by method name
Outbound []*rpcStats // stats for outgoing lsp rpcs sorted by method name
@@ -79,7 +79,7 @@
Count int64
}
-func (r *rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context {
+func (r *Rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context {
r.mu.Lock()
defer r.mu.Unlock()
switch {
@@ -152,7 +152,7 @@
}
}
-func (r *rpcs) getRPCSpan(ctx context.Context, ev core.Event) (*export.Span, *rpcStats) {
+func (r *Rpcs) getRPCSpan(ctx context.Context, ev core.Event) (*export.Span, *rpcStats) {
// get the span
span := export.GetSpan(ctx)
if span == nil {
@@ -163,7 +163,7 @@
return span, r.getRPCStats(span.Start())
}
-func (r *rpcs) getRPCStats(lm label.Map) *rpcStats {
+func (r *Rpcs) getRPCStats(lm label.Map) *rpcStats {
method := tag.Method.Get(lm)
if method == "" {
return nil
@@ -209,7 +209,7 @@
return ""
}
-func (r *rpcs) getData(req *http.Request) interface{} {
+func (r *Rpcs) getData(req *http.Request) interface{} {
return r
}
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index f15d88f..aec976a 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -64,7 +64,7 @@
ocagent *ocagent.Exporter
prometheus *prometheus.Exporter
- rpcs *rpcs
+ rpcs *Rpcs
traces *traces
State *State
}
@@ -349,7 +349,7 @@
ocConfig.Address = i.OCAgentConfig
i.ocagent = ocagent.Connect(ocConfig)
i.prometheus = prometheus.New()
- i.rpcs = &rpcs{}
+ i.rpcs = &Rpcs{}
i.traces = &traces{}
i.State = &State{}
i.exporter = makeInstanceExporter(i)
@@ -405,8 +405,8 @@
event.Log(ctx, "Debug serving", tag.Port.Of(port))
go func() {
mux := http.NewServeMux()
- mux.HandleFunc("/", render(mainTmpl, func(*http.Request) interface{} { return i }))
- mux.HandleFunc("/debug/", render(debugTmpl, nil))
+ mux.HandleFunc("/", render(MainTmpl, func(*http.Request) interface{} { return i }))
+ mux.HandleFunc("/debug/", render(DebugTmpl, nil))
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
@@ -416,19 +416,19 @@
mux.HandleFunc("/metrics/", i.prometheus.Serve)
}
if i.rpcs != nil {
- mux.HandleFunc("/rpc/", render(rpcTmpl, i.rpcs.getData))
+ mux.HandleFunc("/rpc/", render(RPCTmpl, i.rpcs.getData))
}
if i.traces != nil {
- mux.HandleFunc("/trace/", render(traceTmpl, i.traces.getData))
+ mux.HandleFunc("/trace/", render(TraceTmpl, i.traces.getData))
}
- mux.HandleFunc("/cache/", render(cacheTmpl, i.getCache))
- mux.HandleFunc("/session/", render(sessionTmpl, i.getSession))
- mux.HandleFunc("/view/", render(viewTmpl, i.getView))
- mux.HandleFunc("/client/", render(clientTmpl, i.getClient))
- mux.HandleFunc("/server/", render(serverTmpl, i.getServer))
- mux.HandleFunc("/file/", render(fileTmpl, i.getFile))
- mux.HandleFunc("/info", render(infoTmpl, i.getInfo))
- mux.HandleFunc("/memory", render(memoryTmpl, getMemory))
+ mux.HandleFunc("/cache/", render(CacheTmpl, i.getCache))
+ mux.HandleFunc("/session/", render(SessionTmpl, i.getSession))
+ mux.HandleFunc("/view/", render(ViewTmpl, i.getView))
+ mux.HandleFunc("/client/", render(ClientTmpl, i.getClient))
+ mux.HandleFunc("/server/", render(ServerTmpl, i.getServer))
+ mux.HandleFunc("/file/", render(FileTmpl, i.getFile))
+ mux.HandleFunc("/info", render(InfoTmpl, i.getInfo))
+ mux.HandleFunc("/memory", render(MemoryTmpl, getMemory))
if err := http.Serve(listener, mux); err != nil {
event.Error(ctx, "Debug server failed", err)
return
@@ -639,7 +639,7 @@
return string(v)
}
-var baseTemplate = template.Must(template.New("").Parse(`
+var BaseTemplate = template.Must(template.New("").Parse(`
<html>
<head>
<title>{{template "title" .}}</title>
@@ -708,7 +708,7 @@
},
})
-var mainTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls server information{{end}}
{{define "body"}}
<h2>Caches</h2>
@@ -724,14 +724,14 @@
{{end}}
`))
-var infoTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var InfoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls version information{{end}}
{{define "body"}}
{{.}}
{{end}}
`))
-var memoryTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var MemoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls memory usage{{end}}
{{define "head"}}<meta http-equiv="refresh" content="5">{{end}}
{{define "body"}}
@@ -761,14 +761,14 @@
{{end}}
`))
-var debugTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var DebugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls Debug pages{{end}}
{{define "body"}}
<a href="/debug/pprof">Profiling</a>
{{end}}
`))
-var cacheTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var CacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Cache {{.ID}}{{end}}
{{define "body"}}
<h2>memoize.Store entries</h2>
@@ -778,7 +778,7 @@
{{end}}
`))
-var clientTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var ClientTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Client {{.Session.ID}}{{end}}
{{define "body"}}
Using session: <b>{{template "sessionlink" .Session.ID}}</b><br>
@@ -788,7 +788,7 @@
{{end}}
`))
-var serverTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var ServerTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Server {{.ID}}{{end}}
{{define "body"}}
{{if .DebugAddress}}Debug this server at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>{{end}}
@@ -797,7 +797,7 @@
{{end}}
`))
-var sessionTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var SessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Session {{.ID}}{{end}}
{{define "body"}}
From: <b>{{template "cachelink" .Cache.ID}}</b><br>
@@ -810,7 +810,7 @@
{{end}}
`))
-var viewTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var ViewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}View {{.ID}}{{end}}
{{define "body"}}
Name: <b>{{.Name}}</b><br>
@@ -821,7 +821,7 @@
{{end}}
`))
-var fileTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Overlay {{.FileIdentity.Hash}}{{end}}
{{define "body"}}
{{with .}}
diff --git a/internal/lsp/debug/tag/tag.go b/internal/lsp/debug/tag/tag.go
index 15fc680..ff2f2ec 100644
--- a/internal/lsp/debug/tag/tag.go
+++ b/internal/lsp/debug/tag/tag.go
@@ -19,7 +19,7 @@
File = keys.NewString("file", "")
Directory = keys.New("directory", "")
URI = keys.New("URI", "")
- Package = keys.NewString("package", "")
+ Package = keys.NewString("package", "") // Package ID
PackagePath = keys.NewString("package_path", "")
Query = keys.New("query", "")
Snapshot = keys.NewUInt64("snapshot", "")
diff --git a/internal/lsp/debug/trace.go b/internal/lsp/debug/trace.go
index a4e9be9..ca61286 100644
--- a/internal/lsp/debug/trace.go
+++ b/internal/lsp/debug/trace.go
@@ -22,7 +22,7 @@
"golang.org/x/tools/internal/event/label"
)
-var traceTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+var TraceTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Trace Information{{end}}
{{define "body"}}
{{range .Traces}}<a href="/trace/{{.Name}}">{{.Name}}</a> last: {{.Last.Duration}}, longest: {{.Longest.Duration}}<br>{{end}}
@@ -45,7 +45,7 @@
unfinished map[export.SpanContext]*traceData
}
-type traceResults struct {
+type TraceResults struct { // exported for testing
Traces []*traceSet
Selected *traceSet
}
@@ -193,7 +193,7 @@
if len(t.sets) == 0 {
return nil
}
- data := traceResults{}
+ data := TraceResults{}
data.Traces = make([]*traceSet, 0, len(t.sets))
for _, set := range t.sets {
data.Traces = append(data.Traces, set)
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 3fd3dbf..4941f00 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -178,29 +178,13 @@
if s.shouldIgnoreError(ctx, snapshot, err) {
return
}
- // Even if packages didn't fail to load, we still may want to show
- // additional warnings.
- if err == nil {
- if msg := shouldShowAdHocPackagesWarning(snapshot, wsPkgs); msg != "" {
- err = fmt.Errorf(msg)
- }
- }
-
- // Even if workspace packages were returned, there still may be an error
- // with the user's workspace layout. Workspace packages that only have the
- // ID "command-line-arguments" are usually a symptom of a bad workspace
- // configuration.
- if containsCommandLineArguments(wsPkgs) {
- if criticalErr := snapshot.WorkspaceLayoutError(ctx); criticalErr != nil {
- err = criticalErr
- }
- }
+ criticalErr := snapshot.GetCriticalError(ctx)
// Show the error as a progress error report so that it appears in the
// status bar. If a client doesn't support progress reports, the error
// will still be shown as a ShowMessage. If there is no error, any running
// error progress reports will be closed.
- s.showCriticalErrorStatus(ctx, snapshot, err)
+ s.showCriticalErrorStatus(ctx, snapshot, criticalErr)
// If there are no workspace packages, there is nothing to diagnose and
// there are no orphaned files.
@@ -337,27 +321,11 @@
}
}
-const adHocPackagesWarning = `You are outside of a module and outside of $GOPATH/src.
-If you are using modules, please open your editor to a directory in your module.
-If you believe this warning is incorrect, please file an issue: https://github.com/golang/go/issues/new.`
-
-func shouldShowAdHocPackagesWarning(snapshot source.Snapshot, pkgs []source.Package) string {
- if snapshot.ValidBuildConfiguration() {
- return ""
- }
- for _, pkg := range pkgs {
- if len(pkg.MissingDependencies()) > 0 {
- return adHocPackagesWarning
- }
- }
- return ""
-}
-
const WorkspaceLoadFailure = "Error loading workspace"
// showCriticalErrorStatus shows the error as a progress report.
// If the error is nil, it clears any existing error progress report.
-func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Snapshot, err error) {
+func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Snapshot, err *source.CriticalError) {
s.criticalErrorStatusMu.Lock()
defer s.criticalErrorStatusMu.Unlock()
@@ -369,7 +337,7 @@
// Some error messages can also be displayed as diagnostics.
if criticalErr := (*source.CriticalError)(nil); errors.As(err, &criticalErr) {
- s.storeErrorDiagnostics(ctx, snapshot, typeCheckSource, criticalErr.ErrorList)
+ s.storeErrorDiagnostics(ctx, snapshot, modSource, criticalErr.ErrorList)
}
errMsg = strings.Replace(err.Error(), "\n", " ", -1)
}
@@ -578,12 +546,3 @@
})
return !hasGo
}
-
-func containsCommandLineArguments(pkgs []source.Package) bool {
- for _, pkg := range pkgs {
- if strings.Contains(pkg.ID(), "command-line-arguments") {
- return true
- }
- }
- return false
-}
diff --git a/internal/lsp/diff/diff.go b/internal/lsp/diff/diff.go
index 5536c3b..5d8c69c 100644
--- a/internal/lsp/diff/diff.go
+++ b/internal/lsp/diff/diff.go
@@ -21,7 +21,7 @@
// ComputeEdits is the type for a function that produces a set of edits that
// convert from the before content to the after content.
-type ComputeEdits func(uri span.URI, before, after string) []TextEdit
+type ComputeEdits func(uri span.URI, before, after string) ([]TextEdit, error)
// SortTextEdits attempts to order all edits by their starting points.
// The sort is stable so that edits with the same starting point will not
diff --git a/internal/lsp/diff/difftest/difftest.go b/internal/lsp/diff/difftest/difftest.go
index c8a97fa..0e014bc 100644
--- a/internal/lsp/diff/difftest/difftest.go
+++ b/internal/lsp/diff/difftest/difftest.go
@@ -222,7 +222,10 @@
for _, test := range TestCases {
t.Run(test.Name, func(t *testing.T) {
t.Helper()
- edits := compute(span.URIFromPath("/"+test.Name), test.In, test.Out)
+ edits, err := compute(span.URIFromPath("/"+test.Name), test.In, test.Out)
+ if err != nil {
+ t.Fatal(err)
+ }
got := diff.ApplyEdits(test.In, edits)
unified := fmt.Sprint(diff.ToUnified(FileA, FileB, test.In, edits))
if got != test.Out {
diff --git a/internal/lsp/diff/myers/diff.go b/internal/lsp/diff/myers/diff.go
index c50e33a..a594750 100644
--- a/internal/lsp/diff/myers/diff.go
+++ b/internal/lsp/diff/myers/diff.go
@@ -16,7 +16,7 @@
// https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/
// https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2
-func ComputeEdits(uri span.URI, before, after string) []diff.TextEdit {
+func ComputeEdits(uri span.URI, before, after string) ([]diff.TextEdit, error) {
ops := operations(splitLines(before), splitLines(after))
edits := make([]diff.TextEdit, 0, len(ops))
for _, op := range ops {
@@ -32,7 +32,7 @@
}
}
}
- return edits
+ return edits, nil
}
type operation struct {
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index e42c507..3ce4782 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -723,6 +723,9 @@
return errors.Errorf("textDocument/codeAction: %w", err)
}
for _, action := range actions {
+ if action.Title == "" {
+ return errors.Errorf("empty title for code action")
+ }
var match bool
for _, o := range only {
if action.Kind == o {
@@ -754,6 +757,10 @@
return err
}
}
+ // Some commands may edit files on disk.
+ if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
+ return err
+ }
}
return nil
}
@@ -904,6 +911,23 @@
return completions, nil
}
+// AcceptCompletion accepts a completion for the given item at the given
+// position.
+func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos Pos, item protocol.CompletionItem) error {
+ if e.Server == nil {
+ return nil
+ }
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ _, ok := e.buffers[path]
+ if !ok {
+ return fmt.Errorf("buffer %q is not open", path)
+ }
+ return e.editBufferLocked(ctx, path, convertEdits(append([]protocol.TextEdit{
+ *item.TextEdit,
+ }, item.AdditionalTextEdits...)))
+}
+
// References executes a reference request on the server.
func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
if e.Server == nil {
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 81d3168..fd8b4ad 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -453,7 +453,10 @@
return []byte(got), nil
}))
if want != got {
- d := myers.ComputeEdits(uri, want, got)
+ d, err := myers.ComputeEdits(uri, want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
t.Errorf("import failed for %s: %s", filename, diff.ToUnified("want", "got", want, d))
}
}
@@ -549,7 +552,7 @@
return []byte(got), nil
}))
if want != got {
- t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), tests.Diff(want, got))
+ t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got))
}
}
}
@@ -628,7 +631,7 @@
return []byte(got), nil
}))
if want != got {
- t.Errorf("function extraction failed for %s:\n%s", u.Filename(), tests.Diff(want, got))
+ t.Errorf("function extraction failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got))
}
}
}
@@ -680,7 +683,7 @@
return []byte(hover.Contents.Value), nil
}))
if hover.Contents.Value != expectHover {
- t.Errorf("%s:\n%s", d.Src, tests.Diff(expectHover, hover.Contents.Value))
+ t.Errorf("%s:\n%s", d.Src, tests.Diff(t, expectHover, hover.Contents.Value))
}
}
if !d.OnlyHover {
@@ -905,7 +908,7 @@
return []byte(got), nil
}))
if want != got {
- t.Errorf("rename failed for %s:\n%s", newText, tests.Diff(want, got))
+ t.Errorf("rename failed for %s:\n%s", newText, tests.Diff(t, want, got))
}
}
@@ -1052,7 +1055,7 @@
want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
- if diff := tests.Diff(want, got); diff != "" {
+ if diff := tests.Diff(t, want, got); diff != "" {
t.Error(diff)
}
}
@@ -1092,7 +1095,11 @@
if got == nil {
t.Fatalf("expected %v, got nil", want)
}
- if diff := tests.DiffSignatures(spn, want, got); diff != "" {
+ diff, err := tests.DiffSignatures(spn, want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff != "" {
t.Error(diff)
}
}
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index 750b35f..ecd9c65 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -148,7 +148,7 @@
}
firstRequire := pm.File.Require[0].Syntax
- if firstRequire.Start.Byte < start.Byte {
+ if start.Byte == 0 || firstRequire.Start.Byte < start.Byte {
start, end = firstRequire.Start, firstRequire.End
}
return lineToRange(pm.Mapper, fh.URI(), start, end)
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 3b0a564..f57a743 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -13,7 +13,6 @@
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- errors "golang.org/x/xerrors"
)
func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
@@ -66,10 +65,6 @@
return nil, nil
}
if err != nil {
- // Some error messages can also be displayed as diagnostics.
- if criticalErr := (*source.CriticalError)(nil); errors.As(err, &criticalErr) {
- return criticalErr.ErrorList, nil
- }
return nil, err
}
return tidied.Errors, nil
diff --git a/internal/lsp/mod/format.go b/internal/lsp/mod/format.go
index da97ca2..fe22300 100644
--- a/internal/lsp/mod/format.go
+++ b/internal/lsp/mod/format.go
@@ -21,6 +21,9 @@
return nil, err
}
// Calculate the edits to be made due to the change.
- diff := snapshot.View().Options().ComputeEdits(fh.URI(), string(pm.Mapper.Content), string(formatted))
+ diff, err := snapshot.View().Options().ComputeEdits(fh.URI(), string(pm.Mapper.Content), string(formatted))
+ if err != nil {
+ return nil, err
+ }
return source.ToProtocolEdits(pm.Mapper, diff)
}
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index 820268c..da164ed 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -4,106 +4,119 @@
var GeneratedAPIJSON = &APIJSON{
Options: map[string][]*OptionJSON{
- "Debugging": {
- {
- Name: "verboseOutput",
- Type: "bool",
- Doc: "verboseOutput enables additional debug logging.\n",
- EnumValues: nil,
- Default: "false",
- },
- {
- Name: "completionBudget",
- Type: "time.Duration",
- Doc: "completionBudget is the soft latency goal for completion requests. Most\nrequests finish in a couple milliseconds, but in some cases deep\ncompletions can take much longer. As we use up our budget we\ndynamically reduce the search scope to ensure we return timely\nresults. Zero means unlimited.\n",
- EnumValues: nil,
- Default: "\"100ms\"",
- },
- },
- "Experimental": {
- {
- Name: "annotations",
- Type: "map[string]bool",
- Doc: "annotations suppress various kinds of optimization diagnostics\nthat would be reported by the gc_details command.\n * noNilcheck suppresses display of nilchecks.\n * noEscape suppresses escape choices.\n * noInline suppresses inlining choices.\n * noBounds suppresses bounds checking diagnostics.\n",
- EnumValues: nil,
- Default: "{}",
- },
- {
- Name: "staticcheck",
- Type: "bool",
- Doc: "staticcheck enables additional analyses from staticcheck.io.\n",
- EnumValues: nil,
- Default: "false",
- },
- {
- Name: "semanticTokens",
- Type: "bool",
- Doc: "semanticTokens controls whether the LSP server will send\nsemantic tokens to the client.\n",
- EnumValues: nil,
- Default: "false",
- },
- {
- Name: "expandWorkspaceToModule",
- Type: "bool",
- Doc: "expandWorkspaceToModule instructs `gopls` to adjust the scope of the\nworkspace to find the best available module root. `gopls` first looks for\na go.mod file in any parent directory of the workspace folder, expanding\nthe scope to that directory if it exists. If no viable parent directory is\nfound, gopls will check if there is exactly one child directory containing\na go.mod file, narrowing the scope to that directory if it exists.\n",
- EnumValues: nil,
- Default: "true",
- },
- {
- Name: "experimentalWorkspaceModule",
- Type: "bool",
- Doc: "experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n",
- EnumValues: nil,
- Default: "false",
- },
- {
- Name: "experimentalDiagnosticsDelay",
- Type: "time.Duration",
- Doc: "experimentalDiagnosticsDelay controls the amount of time that gopls waits\nafter the most recent file modification before computing deep diagnostics.\nSimple diagnostics (parsing and type-checking) are always run immediately\non recently modified packages.\n\nThis option must be set to a valid duration string, for example `\"250ms\"`.\n",
- EnumValues: nil,
- Default: "\"250ms\"",
- },
- {
- Name: "experimentalPackageCacheKey",
- Type: "bool",
- Doc: "experimentalPackageCacheKey controls whether to use a coarser cache key\nfor package type information to increase cache hits. This setting removes\nthe user's environment, build flags, and working directory from the cache\nkey, which should be a safe change as all relevant inputs into the type\nchecking pass are already hashed into the key. This is temporarily guarded\nby an experiment because caching behavior is subtle and difficult to\ncomprehensively test.\n",
- EnumValues: nil,
- Default: "true",
- },
- {
- Name: "allowModfileModifications",
- Type: "bool",
- Doc: "allowModfileModifications disables -mod=readonly, allowing imports from\nout-of-scope modules. This option will eventually be removed.\n",
- EnumValues: nil,
- Default: "false",
- },
- {
- Name: "allowImplicitNetworkAccess",
- Type: "bool",
- Doc: "allowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module\ndownloads rather than requiring user action. This option will eventually\nbe removed.\n",
- EnumValues: nil,
- Default: "false",
- },
- },
"User": {
{
- Name: "buildFlags",
- Type: "[]string",
- Doc: "buildFlags is the set of flags passed on to the build system when invoked.\nIt is applied to queries like `go list`, which is used when discovering files.\nThe most common use is to set `-tags`.\n",
+ Name: "buildFlags",
+ Type: "[]string",
+ Doc: "buildFlags is the set of flags passed on to the build system when invoked.\nIt is applied to queries like `go list`, which is used when discovering files.\nThe most common use is to set `-tags`.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: nil,
Default: "[]",
+ Status: "",
+ Hierarchy: "build",
},
{
- Name: "env",
- Type: "map[string]string",
- Doc: "env adds environment variables to external commands run by `gopls`, most notably `go list`.\n",
+ Name: "env",
+ Type: "map[string]string",
+ Doc: "env adds environment variables to external commands run by `gopls`, most notably `go list`.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: nil,
Default: "{}",
+ Status: "",
+ Hierarchy: "build",
+ },
+ {
+ Name: "directoryFilters",
+ Type: "[]string",
+ Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nExamples:\nExclude node_modules: `-node_modules`\nInclude only project_a: `-` (exclude everything), `+project_a`\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "[]",
+ Status: "",
+ Hierarchy: "build",
+ },
+ {
+ Name: "expandWorkspaceToModule",
+ Type: "bool",
+ Doc: "expandWorkspaceToModule instructs `gopls` to adjust the scope of the\nworkspace to find the best available module root. `gopls` first looks for\na go.mod file in any parent directory of the workspace folder, expanding\nthe scope to that directory if it exists. If no viable parent directory is\nfound, gopls will check if there is exactly one child directory containing\na go.mod file, narrowing the scope to that directory if it exists.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "true",
+ Status: "experimental",
+ Hierarchy: "build",
+ },
+ {
+ Name: "experimentalWorkspaceModule",
+ Type: "bool",
+ Doc: "experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "experimental",
+ Hierarchy: "build",
+ },
+ {
+ Name: "experimentalPackageCacheKey",
+ Type: "bool",
+ Doc: "experimentalPackageCacheKey controls whether to use a coarser cache key\nfor package type information to increase cache hits. This setting removes\nthe user's environment, build flags, and working directory from the cache\nkey, which should be a safe change as all relevant inputs into the type\nchecking pass are already hashed into the key. This is temporarily guarded\nby an experiment because caching behavior is subtle and difficult to\ncomprehensively test.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "true",
+ Status: "experimental",
+ Hierarchy: "build",
+ },
+ {
+ Name: "allowModfileModifications",
+ Type: "bool",
+ Doc: "allowModfileModifications disables -mod=readonly, allowing imports from\nout-of-scope modules. This option will eventually be removed.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "experimental",
+ Hierarchy: "build",
+ },
+ {
+ Name: "allowImplicitNetworkAccess",
+ Type: "bool",
+ Doc: "allowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module\ndownloads rather than requiring user action. This option will eventually\nbe removed.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "experimental",
+ Hierarchy: "build",
},
{
Name: "hoverKind",
Type: "enum",
Doc: "hoverKind controls the information that appears in the hover text.\nSingleLine and Structured are intended for use only by authors of editor plugins.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: []EnumValue{
{
Value: "\"FullDocumentation\"",
@@ -126,61 +139,96 @@
Doc: "",
},
},
- Default: "\"FullDocumentation\"",
+ Default: "\"FullDocumentation\"",
+ Status: "",
+ Hierarchy: "ui.documentation",
},
{
- Name: "usePlaceholders",
- Type: "bool",
- Doc: "placeholders enables placeholders for function parameters or struct fields in completion responses.\n",
- EnumValues: nil,
- Default: "false",
- },
- {
- Name: "linkTarget",
- Type: "string",
- Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n",
+ Name: "linkTarget",
+ Type: "string",
+ Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: nil,
Default: "\"pkg.go.dev\"",
+ Status: "",
+ Hierarchy: "ui.documentation",
},
{
- Name: "local",
- Type: "string",
- Doc: "local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.\nIt should be the prefix of the import path whose imports should be grouped separately.\n",
- EnumValues: nil,
- Default: "\"\"",
- },
- {
- Name: "gofumpt",
- Type: "bool",
- Doc: "gofumpt indicates if we should run gofumpt formatting.\n",
- EnumValues: nil,
- Default: "false",
- },
- {
- Name: "analyses",
- Type: "map[string]bool",
- Doc: "analyses specify analyses that the user would like to enable or disable.\nA map of the names of analysis passes that should be enabled/disabled.\nA full list of analyzers that gopls uses can be found [here](analyzers.md)\n\nExample Usage:\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedparams\": true // Enable the unusedparams analyzer.\n}\n...\n```\n",
- EnumValues: nil,
- Default: "{}",
- },
- {
- Name: "codelenses",
- Type: "map[string]bool",
- Doc: "codelenses overrides the enabled/disabled state of code lenses. See the \"Code Lenses\"\nsection of settings.md for the list of supported lenses.\n\nExample Usage:\n```json5\n\"gopls\": {\n...\n \"codelens\": {\n \"generate\": false, // Don't show the `go generate` lens.\n \"gc_details\": true // Show a code lens toggling the display of gc's choices.\n }\n...\n}\n```\n",
- EnumValues: nil,
- Default: "{\"gc_details\":false,\"generate\":true,\"regenerate_cgo\":true,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}",
- },
- {
- Name: "linksInHover",
- Type: "bool",
- Doc: "linksInHover toggles the presence of links to documentation in hover.\n",
+ Name: "linksInHover",
+ Type: "bool",
+ Doc: "linksInHover toggles the presence of links to documentation in hover.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: nil,
Default: "true",
+ Status: "",
+ Hierarchy: "ui.documentation",
+ },
+ {
+ Name: "usePlaceholders",
+ Type: "bool",
+ Doc: "placeholders enables placeholders for function parameters or struct\nfields in completion responses.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "",
+ Hierarchy: "ui.completion",
+ },
+ {
+ Name: "completionBudget",
+ Type: "time.Duration",
+ Doc: "completionBudget is the soft latency goal for completion requests. Most\nrequests finish in a couple milliseconds, but in some cases deep\ncompletions can take much longer. As we use up our budget we\ndynamically reduce the search scope to ensure we return timely\nresults. Zero means unlimited.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "\"100ms\"",
+ Status: "debug",
+ Hierarchy: "ui.completion",
+ },
+ {
+ Name: "matcher",
+ Type: "enum",
+ Doc: "matcher sets the algorithm that is used when calculating completion\ncandidates.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: []EnumValue{
+ {
+ Value: "\"CaseInsensitive\"",
+ Doc: "",
+ },
+ {
+ Value: "\"CaseSensitive\"",
+ Doc: "",
+ },
+ {
+ Value: "\"Fuzzy\"",
+ Doc: "",
+ },
+ },
+ Default: "\"Fuzzy\"",
+ Status: "advanced",
+ Hierarchy: "ui.completion",
},
{
Name: "importShortcut",
Type: "enum",
Doc: "importShortcut specifies whether import statements should link to\ndocumentation or go to definitions.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: []EnumValue{
{
Value: "\"Both\"",
@@ -195,32 +243,18 @@
Doc: "",
},
},
- Default: "\"Both\"",
- },
- {
- Name: "matcher",
- Type: "enum",
- Doc: "matcher sets the algorithm that is used when calculating completion candidates.\n",
- EnumValues: []EnumValue{
- {
- Value: "\"CaseInsensitive\"",
- Doc: "",
- },
- {
- Value: "\"CaseSensitive\"",
- Doc: "",
- },
- {
- Value: "\"Fuzzy\"",
- Doc: "",
- },
- },
- Default: "\"Fuzzy\"",
+ Default: "\"Both\"",
+ Status: "",
+ Hierarchy: "ui.navigation",
},
{
Name: "symbolMatcher",
Type: "enum",
Doc: "symbolMatcher sets the algorithm that is used when finding workspace symbols.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: []EnumValue{
{
Value: "\"CaseInsensitive\"",
@@ -235,12 +269,18 @@
Doc: "",
},
},
- Default: "\"Fuzzy\"",
+ Default: "\"Fuzzy\"",
+ Status: "advanced",
+ Hierarchy: "ui.navigation",
},
{
Name: "symbolStyle",
Type: "enum",
- Doc: "symbolStyle controls how symbols are qualified in symbol responses.\n\nExample Usage:\n```json5\n\"gopls\": {\n...\n \"symbolStyle\": \"dynamic\",\n...\n}\n```\n",
+ Doc: "symbolStyle controls how symbols are qualified in symbol responses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"symbolStyle\": \"dynamic\",\n...\n}\n```\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
EnumValues: []EnumValue{
{
Value: "\"Dynamic\"",
@@ -255,14 +295,379 @@
Doc: "`\"Package\"` is package qualified symbols i.e.\n\"pkg.Foo.Field\".\n",
},
},
- Default: "\"Dynamic\"",
+ Default: "\"Dynamic\"",
+ Status: "advanced",
+ Hierarchy: "ui.navigation",
},
{
- Name: "directoryFilters",
- Type: "[]string",
- Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nExamples:\nExclude node_modules: `-node_modules`\nInclude only project_a: `-` (exclude everything), `+project_a`\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n",
+ Name: "analyses",
+ Type: "map[string]bool",
+ Doc: "analyses specify analyses that the user would like to enable or disable.\nA map of the names of analysis passes that should be enabled/disabled.\nA full list of analyzers that gopls uses can be found\n[here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).\n\nExample Usage:\n\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedparams\": true // Enable the unusedparams analyzer.\n}\n...\n```\n",
+ EnumKeys: EnumKeys{
+ ValueType: "bool",
+ Keys: []EnumKey{
+ {
+ Name: "\"asmdecl\"",
+ Doc: "report mismatches between assembly files and Go declarations",
+ Default: "true",
+ },
+ {
+ Name: "\"assign\"",
+ Doc: "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.",
+ Default: "true",
+ },
+ {
+ Name: "\"atomic\"",
+ Doc: "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(&x, 1)\n\nwhich are not atomic.",
+ Default: "true",
+ },
+ {
+ Name: "\"atomicalign\"",
+ Doc: "check for non-64-bits-aligned arguments to sync/atomic functions",
+ Default: "true",
+ },
+ {
+ Name: "\"bools\"",
+ Doc: "check for common mistakes involving boolean operators",
+ Default: "true",
+ },
+ {
+ Name: "\"buildtag\"",
+ Doc: "check that +build tags are well-formed and correctly located",
+ Default: "true",
+ },
+ {
+ Name: "\"cgocall\"",
+ Doc: "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.",
+ Default: "true",
+ },
+ {
+ Name: "\"composites\"",
+ Doc: "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = &net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = &net.DNSConfigError{Err: err}\n",
+ Default: "true",
+ },
+ {
+ Name: "\"copylocks\"",
+ Doc: "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.",
+ Default: "true",
+ },
+ {
+ Name: "\"deepequalerrors\"",
+ Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.",
+ Default: "true",
+ },
+ {
+ Name: "\"errorsas\"",
+ Doc: "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.",
+ Default: "true",
+ },
+ {
+ Name: "\"fieldalignment\"",
+ Doc: "find structs that would take less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to take less memory, and provides\na suggested edit with the optimal order.\n",
+ Default: "false",
+ },
+ {
+ Name: "\"httpresponse\"",
+ Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.",
+ Default: "true",
+ },
+ {
+ Name: "\"ifaceassert\"",
+ Doc: "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"loopclosure\"",
+ Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a\nfunction literal inside the loop body. It checks only instances where\nthe function literal is called in a defer or go statement that is the\nlast statement in the loop body, as otherwise we would need whole\nprogram analysis.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines",
+ Default: "true",
+ },
+ {
+ Name: "\"lostcancel\"",
+ Doc: "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nand WithDeadline must be called or the new context will remain live\nuntil its parent context is cancelled.\n(The background context is never cancelled.)",
+ Default: "true",
+ },
+ {
+ Name: "\"nilfunc\"",
+ Doc: "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.",
+ Default: "true",
+ },
+ {
+ Name: "\"printf\"",
+ Doc: "check consistency of Printf format strings and arguments\n\nThe check applies to known functions (for example, those in package fmt)\nas well as any detected wrappers of known functions.\n\nA function that wants to avail itself of printf checking but is not\nfound by this analyzer's heuristics (for example, due to use of\ndynamic calls) can insert a bogus call:\n\n\tif false {\n\t\t_ = fmt.Sprintf(format, args...) // enable printf checking\n\t}\n\nThe -funcs flag specifies a comma-separated list of names of additional\nknown formatting functions or methods. If the name contains a period,\nit must denote a specific function using one of the following forms:\n\n\tdir/pkg.Function\n\tdir/pkg.Type.Method\n\t(*dir/pkg.Type).Method\n\nOtherwise the name is interpreted as a case-insensitive unqualified\nidentifier such as \"errorf\". Either way, if a listed name ends in f, the\nfunction is assumed to be Printf-like, taking a format string before the\nargument list. Otherwise it is assumed to be Print-like, taking a list\nof arguments with no format string.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"shadow\"",
+ Doc: "check for possible unintended shadowing of variables\n\nThis analyzer check for shadowed variables.\nA shadowed variable is a variable declared in an inner scope\nwith the same name and type as a variable in an outer scope,\nand where the outer variable is mentioned after the inner one\nis declared.\n\n(This definition can be refined; the module generates too many\nfalse positives and is not yet enabled by default.)\n\nFor example:\n\n\tfunc BadRead(f *os.File, buf []byte) error {\n\t\tvar err error\n\t\tfor {\n\t\t\tn, err := f.Read(buf) // shadows the function variable 'err'\n\t\t\tif err != nil {\n\t\t\t\tbreak // causes return of wrong value\n\t\t\t}\n\t\t\tfoo(buf)\n\t\t}\n\t\treturn err\n\t}\n",
+ Default: "false",
+ },
+ {
+ Name: "\"shift\"",
+ Doc: "check for shifts that equal or exceed the width of the integer",
+ Default: "true",
+ },
+ {
+ Name: "\"simplifycompositelit\"",
+ Doc: "check for composite literal simplifications\n\nAn array, slice, or map composite literal of the form:\n\t[]T{T{}, T{}}\nwill be simplified to:\n\t[]T{{}, {}}\n\nThis is one of the simplifications that \"gofmt -s\" applies.",
+ Default: "true",
+ },
+ {
+ Name: "\"simplifyrange\"",
+ Doc: "check for range statement simplifications\n\nA range of the form:\n\tfor x, _ = range v {...}\nwill be simplified to:\n\tfor x = range v {...}\n\nA range of the form:\n\tfor _ = range v {...}\nwill be simplified to:\n\tfor range v {...}\n\nThis is one of the simplifications that \"gofmt -s\" applies.",
+ Default: "true",
+ },
+ {
+ Name: "\"simplifyslice\"",
+ Doc: "check for slice simplifications\n\nA slice expression of the form:\n\ts[a:len(s)]\nwill be simplified to:\n\ts[a:]\n\nThis is one of the simplifications that \"gofmt -s\" applies.",
+ Default: "true",
+ },
+ {
+ Name: "\"sortslice\"",
+ Doc: "check the argument type of sort.Slice\n\nsort.Slice requires an argument of a slice type. Check that\nthe interface{} value passed to sort.Slice is actually a slice.",
+ Default: "true",
+ },
+ {
+ Name: "\"stdmethods\"",
+ Doc: "check signature of methods of well-known interfaces\n\nSometimes a type may be intended to satisfy an interface but may fail to\ndo so because of a mistake in its method signature.\nFor example, the result of this WriteTo method should be (int64, error),\nnot error, to satisfy io.WriterTo:\n\n\ttype myWriterTo struct{...}\n func (myWriterTo) WriteTo(w io.Writer) error { ... }\n\nThis check ensures that each method whose name matches one of several\nwell-known interface methods from the standard library has the correct\nsignature for that interface.\n\nChecked method names include:\n\tFormat GobEncode GobDecode MarshalJSON MarshalXML\n\tPeek ReadByte ReadFrom ReadRune Scan Seek\n\tUnmarshalJSON UnreadByte UnreadRune WriteByte\n\tWriteTo\n",
+ Default: "true",
+ },
+ {
+ Name: "\"stringintconv\"",
+ Doc: "check for string(int) conversions\n\nThis checker flags conversions of the form string(x) where x is an integer\n(but not byte or rune) type. Such conversions are discouraged because they\nreturn the UTF-8 representation of the Unicode code point x, and not a decimal\nstring representation of x as one might expect. Furthermore, if x denotes an\ninvalid code point, the conversion cannot be statically rejected.\n\nFor conversions that intend on using the code point, consider replacing them\nwith string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the\nstring representation of the value in the desired base.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"structtag\"",
+ Doc: "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.",
+ Default: "true",
+ },
+ {
+ Name: "\"testinggoroutine\"",
+ Doc: "report calls to (*testing.T).Fatal from goroutines started by a test.\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\nfunc TestFoo(t *testing.T) {\n go func() {\n t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n }()\n}\n",
+ Default: "true",
+ },
+ {
+ Name: "\"tests\"",
+ Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.",
+ Default: "true",
+ },
+ {
+ Name: "\"unmarshal\"",
+ Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.",
+ Default: "true",
+ },
+ {
+ Name: "\"unreachable\"",
+ Doc: "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by an return statement, a call to panic, an\ninfinite loop, or similar constructs.",
+ Default: "true",
+ },
+ {
+ Name: "\"unsafeptr\"",
+ Doc: "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.",
+ Default: "true",
+ },
+ {
+ Name: "\"unusedparams\"",
+ Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo reduce false positives it ignores:\n- methods\n- parameters that do not have a name or are underscored\n- functions in test files\n- functions with empty bodies or those with just a return stmt",
+ Default: "false",
+ },
+ {
+ Name: "\"unusedresult\"",
+ Doc: "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side effects,\nso it is always a mistake to discard the result. This analyzer reports\ncalls to certain functions in which the result of the call is ignored.\n\nThe set of functions may be controlled using flags.",
+ Default: "true",
+ },
+ {
+ Name: "\"fillreturns\"",
+ Doc: "suggested fixes for \"wrong number of return values (want %d, got %d)\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\nwill turn into\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"nonewvars\"",
+ Doc: "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\tz := 1\n\tz := 2\nwill turn into\n\tz := 1\n\tz = 2\n",
+ Default: "true",
+ },
+ {
+ Name: "\"noresultvalues\"",
+ Doc: "suggested fixes for \"no result values expected\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\". For example:\n\tfunc z() { return nil }\nwill turn into\n\tfunc z() { return }\n",
+ Default: "true",
+ },
+ {
+ Name: "\"undeclaredname\"",
+ Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will insert a new statement:\n\"<> := \".",
+ Default: "true",
+ },
+ {
+ Name: "\"fillstruct\"",
+ Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n",
+ Default: "true",
+ },
+ },
+ },
EnumValues: nil,
- Default: "[]",
+ Default: "{}",
+ Status: "",
+ Hierarchy: "ui.diagnostic",
+ },
+ {
+ Name: "staticcheck",
+ Type: "bool",
+ Doc: "staticcheck enables additional analyses from staticcheck.io.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "experimental",
+ Hierarchy: "ui.diagnostic",
+ },
+ {
+ Name: "annotations",
+ Type: "map[string]bool",
+ Doc: "annotations specifies the various kinds of optimization diagnostics\nthat should be reported by the gc_details command.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "bool",
+ Keys: []EnumKey{
+ {
+ Name: "\"bounds\"",
+ Doc: "`\"bounds\"` controls bounds checking diagnostics.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"escape\"",
+ Doc: "`\"escape\"` controls diagnostics about escape choices.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"inline\"",
+ Doc: "`\"inline\"` controls diagnostics about inlining choices.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"nil\"",
+ Doc: "`\"nil\"` controls nil checks.\n",
+ Default: "true",
+ },
+ },
+ },
+ EnumValues: nil,
+ Default: "{\"bounds\":true,\"escape\":true,\"inline\":true,\"nil\":true}",
+ Status: "experimental",
+ Hierarchy: "ui.diagnostic",
+ },
+ {
+ Name: "experimentalDiagnosticsDelay",
+ Type: "time.Duration",
+ Doc: "experimentalDiagnosticsDelay controls the amount of time that gopls waits\nafter the most recent file modification before computing deep diagnostics.\nSimple diagnostics (parsing and type-checking) are always run immediately\non recently modified packages.\n\nThis option must be set to a valid duration string, for example `\"250ms\"`.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "\"250ms\"",
+ Status: "experimental",
+ Hierarchy: "ui.diagnostic",
+ },
+ {
+ Name: "codelenses",
+ Type: "map[string]bool",
+ Doc: "codelenses overrides the enabled/disabled state of code lenses. See the\n\"Code Lenses\" section of the\n[Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md)\nfor the list of supported lenses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"codelens\": {\n \"generate\": false, // Don't show the `go generate` lens.\n \"gc_details\": true // Show a code lens toggling the display of gc's choices.\n }\n...\n}\n```\n",
+ EnumKeys: EnumKeys{
+ ValueType: "bool",
+ Keys: []EnumKey{
+ {
+ Name: "\"generate\"",
+ Doc: "generate runs `go generate` for a given directory.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"regenerate_cgo\"",
+ Doc: "regenerate_cgo regenerates cgo definitions.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"test\"",
+ Doc: "test runs `go test` for a specific test function.\n",
+ Default: "false",
+ },
+ {
+ Name: "\"tidy\"",
+ Doc: "tidy runs `go mod tidy` for a module.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"upgrade_dependency\"",
+ Doc: "upgrade_dependency upgrades a dependency.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"vendor\"",
+ Doc: "vendor runs `go mod vendor` for a module.\n",
+ Default: "true",
+ },
+ {
+ Name: "\"gc_details\"",
+ Doc: "gc_details controls calculation of gc annotations.\n",
+ Default: "false",
+ },
+ },
+ },
+ EnumValues: nil,
+ Default: "{\"gc_details\":false,\"generate\":true,\"regenerate_cgo\":true,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}",
+ Status: "",
+ Hierarchy: "ui",
+ },
+ {
+ Name: "semanticTokens",
+ Type: "bool",
+ Doc: "semanticTokens controls whether the LSP server will send\nsemantic tokens to the client.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "experimental",
+ Hierarchy: "ui",
+ },
+ {
+ Name: "local",
+ Type: "string",
+ Doc: "local is the equivalent of the `goimports -local` flag, which puts\nimports beginning with this string after third-party packages. It should\nbe the prefix of the import path whose imports should be grouped\nseparately.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "\"\"",
+ Status: "",
+ Hierarchy: "formatting",
+ },
+ {
+ Name: "gofumpt",
+ Type: "bool",
+ Doc: "gofumpt indicates if we should run gofumpt formatting.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "",
+ Hierarchy: "formatting",
+ },
+ {
+ Name: "verboseOutput",
+ Type: "bool",
+ Doc: "verboseOutput enables additional debug logging.\n",
+ EnumKeys: EnumKeys{
+ ValueType: "",
+ Keys: nil,
+ },
+ EnumValues: nil,
+ Default: "false",
+ Status: "debug",
+ Hierarchy: "",
},
},
},
@@ -385,4 +790,201 @@
Doc: "gc_details controls calculation of gc annotations.\n",
},
},
+ Analyzers: []*AnalyzerJSON{
+ {
+ Name: "asmdecl",
+ Doc: "report mismatches between assembly files and Go declarations",
+ Default: true,
+ },
+ {
+ Name: "assign",
+ Doc: "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.",
+ Default: true,
+ },
+ {
+ Name: "atomic",
+ Doc: "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(&x, 1)\n\nwhich are not atomic.",
+ Default: true,
+ },
+ {
+ Name: "atomicalign",
+ Doc: "check for non-64-bits-aligned arguments to sync/atomic functions",
+ Default: true,
+ },
+ {
+ Name: "bools",
+ Doc: "check for common mistakes involving boolean operators",
+ Default: true,
+ },
+ {
+ Name: "buildtag",
+ Doc: "check that +build tags are well-formed and correctly located",
+ Default: true,
+ },
+ {
+ Name: "cgocall",
+ Doc: "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.",
+ Default: true,
+ },
+ {
+ Name: "composites",
+ Doc: "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = &net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = &net.DNSConfigError{Err: err}\n",
+ Default: true,
+ },
+ {
+ Name: "copylocks",
+ Doc: "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.",
+ Default: true,
+ },
+ {
+ Name: "deepequalerrors",
+ Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.",
+ Default: true,
+ },
+ {
+ Name: "errorsas",
+ Doc: "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.",
+ Default: true,
+ },
+ {
+ Name: "fieldalignment",
+ Doc: "find structs that would take less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to take less memory, and provides\na suggested edit with the optimal order.\n",
+ Default: false,
+ },
+ {
+ Name: "httpresponse",
+ Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.",
+ Default: true,
+ },
+ {
+ Name: "ifaceassert",
+ Doc: "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.\n",
+ Default: true,
+ },
+ {
+ Name: "loopclosure",
+ Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a\nfunction literal inside the loop body. It checks only instances where\nthe function literal is called in a defer or go statement that is the\nlast statement in the loop body, as otherwise we would need whole\nprogram analysis.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines",
+ Default: true,
+ },
+ {
+ Name: "lostcancel",
+ Doc: "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nand WithDeadline must be called or the new context will remain live\nuntil its parent context is cancelled.\n(The background context is never cancelled.)",
+ Default: true,
+ },
+ {
+ Name: "nilfunc",
+ Doc: "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.",
+ Default: true,
+ },
+ {
+ Name: "printf",
+ Doc: "check consistency of Printf format strings and arguments\n\nThe check applies to known functions (for example, those in package fmt)\nas well as any detected wrappers of known functions.\n\nA function that wants to avail itself of printf checking but is not\nfound by this analyzer's heuristics (for example, due to use of\ndynamic calls) can insert a bogus call:\n\n\tif false {\n\t\t_ = fmt.Sprintf(format, args...) // enable printf checking\n\t}\n\nThe -funcs flag specifies a comma-separated list of names of additional\nknown formatting functions or methods. If the name contains a period,\nit must denote a specific function using one of the following forms:\n\n\tdir/pkg.Function\n\tdir/pkg.Type.Method\n\t(*dir/pkg.Type).Method\n\nOtherwise the name is interpreted as a case-insensitive unqualified\nidentifier such as \"errorf\". Either way, if a listed name ends in f, the\nfunction is assumed to be Printf-like, taking a format string before the\nargument list. Otherwise it is assumed to be Print-like, taking a list\nof arguments with no format string.\n",
+ Default: true,
+ },
+ {
+ Name: "shadow",
+ Doc: "check for possible unintended shadowing of variables\n\nThis analyzer check for shadowed variables.\nA shadowed variable is a variable declared in an inner scope\nwith the same name and type as a variable in an outer scope,\nand where the outer variable is mentioned after the inner one\nis declared.\n\n(This definition can be refined; the module generates too many\nfalse positives and is not yet enabled by default.)\n\nFor example:\n\n\tfunc BadRead(f *os.File, buf []byte) error {\n\t\tvar err error\n\t\tfor {\n\t\t\tn, err := f.Read(buf) // shadows the function variable 'err'\n\t\t\tif err != nil {\n\t\t\t\tbreak // causes return of wrong value\n\t\t\t}\n\t\t\tfoo(buf)\n\t\t}\n\t\treturn err\n\t}\n",
+ Default: false,
+ },
+ {
+ Name: "shift",
+ Doc: "check for shifts that equal or exceed the width of the integer",
+ Default: true,
+ },
+ {
+ Name: "simplifycompositelit",
+ Doc: "check for composite literal simplifications\n\nAn array, slice, or map composite literal of the form:\n\t[]T{T{}, T{}}\nwill be simplified to:\n\t[]T{{}, {}}\n\nThis is one of the simplifications that \"gofmt -s\" applies.",
+ Default: true,
+ },
+ {
+ Name: "simplifyrange",
+ Doc: "check for range statement simplifications\n\nA range of the form:\n\tfor x, _ = range v {...}\nwill be simplified to:\n\tfor x = range v {...}\n\nA range of the form:\n\tfor _ = range v {...}\nwill be simplified to:\n\tfor range v {...}\n\nThis is one of the simplifications that \"gofmt -s\" applies.",
+ Default: true,
+ },
+ {
+ Name: "simplifyslice",
+ Doc: "check for slice simplifications\n\nA slice expression of the form:\n\ts[a:len(s)]\nwill be simplified to:\n\ts[a:]\n\nThis is one of the simplifications that \"gofmt -s\" applies.",
+ Default: true,
+ },
+ {
+ Name: "sortslice",
+ Doc: "check the argument type of sort.Slice\n\nsort.Slice requires an argument of a slice type. Check that\nthe interface{} value passed to sort.Slice is actually a slice.",
+ Default: true,
+ },
+ {
+ Name: "stdmethods",
+ Doc: "check signature of methods of well-known interfaces\n\nSometimes a type may be intended to satisfy an interface but may fail to\ndo so because of a mistake in its method signature.\nFor example, the result of this WriteTo method should be (int64, error),\nnot error, to satisfy io.WriterTo:\n\n\ttype myWriterTo struct{...}\n func (myWriterTo) WriteTo(w io.Writer) error { ... }\n\nThis check ensures that each method whose name matches one of several\nwell-known interface methods from the standard library has the correct\nsignature for that interface.\n\nChecked method names include:\n\tFormat GobEncode GobDecode MarshalJSON MarshalXML\n\tPeek ReadByte ReadFrom ReadRune Scan Seek\n\tUnmarshalJSON UnreadByte UnreadRune WriteByte\n\tWriteTo\n",
+ Default: true,
+ },
+ {
+ Name: "stringintconv",
+ Doc: "check for string(int) conversions\n\nThis checker flags conversions of the form string(x) where x is an integer\n(but not byte or rune) type. Such conversions are discouraged because they\nreturn the UTF-8 representation of the Unicode code point x, and not a decimal\nstring representation of x as one might expect. Furthermore, if x denotes an\ninvalid code point, the conversion cannot be statically rejected.\n\nFor conversions that intend on using the code point, consider replacing them\nwith string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the\nstring representation of the value in the desired base.\n",
+ Default: true,
+ },
+ {
+ Name: "structtag",
+ Doc: "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.",
+ Default: true,
+ },
+ {
+ Name: "testinggoroutine",
+ Doc: "report calls to (*testing.T).Fatal from goroutines started by a test.\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\nfunc TestFoo(t *testing.T) {\n go func() {\n t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n }()\n}\n",
+ Default: true,
+ },
+ {
+ Name: "tests",
+ Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.",
+ Default: true,
+ },
+ {
+ Name: "unmarshal",
+ Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.",
+ Default: true,
+ },
+ {
+ Name: "unreachable",
+ Doc: "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by an return statement, a call to panic, an\ninfinite loop, or similar constructs.",
+ Default: true,
+ },
+ {
+ Name: "unsafeptr",
+ Doc: "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.",
+ Default: true,
+ },
+ {
+ Name: "unusedparams",
+ Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo reduce false positives it ignores:\n- methods\n- parameters that do not have a name or are underscored\n- functions in test files\n- functions with empty bodies or those with just a return stmt",
+ Default: false,
+ },
+ {
+ Name: "unusedresult",
+ Doc: "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side effects,\nso it is always a mistake to discard the result. This analyzer reports\ncalls to certain functions in which the result of the call is ignored.\n\nThe set of functions may be controlled using flags.",
+ Default: true,
+ },
+ {
+ Name: "fillreturns",
+ Doc: "suggested fixes for \"wrong number of return values (want %d, got %d)\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\nwill turn into\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.\n",
+ Default: true,
+ },
+ {
+ Name: "nonewvars",
+ Doc: "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\tz := 1\n\tz := 2\nwill turn into\n\tz := 1\n\tz = 2\n",
+ Default: true,
+ },
+ {
+ Name: "noresultvalues",
+ Doc: "suggested fixes for \"no result values expected\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\". For example:\n\tfunc z() { return nil }\nwill turn into\n\tfunc z() { return }\n",
+ Default: true,
+ },
+ {
+ Name: "undeclaredname",
+ Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will insert a new statement:\n\"<> := \".",
+ Default: true,
+ },
+ {
+ Name: "fillstruct",
+ Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n",
+ Default: true,
+ },
+ },
}
diff --git a/internal/lsp/source/call_hierarchy.go b/internal/lsp/source/call_hierarchy.go
index 7a521ed..c7dfea5 100644
--- a/internal/lsp/source/call_hierarchy.go
+++ b/internal/lsp/source/call_hierarchy.go
@@ -193,7 +193,9 @@
if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
return nil, nil
}
-
+ if identifier.Declaration.node == nil {
+ return nil, nil
+ }
if len(identifier.Declaration.MappedRange) == 0 {
return nil, nil
}
diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go
index d6d4491..1dd66b4 100644
--- a/internal/lsp/source/code_lens.go
+++ b/internal/lsp/source/code_lens.go
@@ -71,27 +71,29 @@
})
}
- _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
- if err != nil {
- return nil, err
+ if len(fns.Benchmarks) > 0 {
+ _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
+ if err != nil {
+ return nil, err
+ }
+ // add a code lens to the top of the file which runs all benchmarks in the file
+ rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
+ if err != nil {
+ return nil, err
+ }
+ args, err := MarshalArgs(fh.URI(), []string{}, fns.Benchmarks)
+ if err != nil {
+ return nil, err
+ }
+ codeLens = append(codeLens, protocol.CodeLens{
+ Range: rng,
+ Command: protocol.Command{
+ Title: "run file benchmarks",
+ Command: CommandTest.ID(),
+ Arguments: args,
+ },
+ })
}
- // add a code lens to the top of the file which runs all benchmarks in the file
- rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
- if err != nil {
- return nil, err
- }
- args, err := MarshalArgs(fh.URI(), []string{}, fns.Benchmarks)
- if err != nil {
- return nil, err
- }
- codeLens = append(codeLens, protocol.CodeLens{
- Range: rng,
- Command: protocol.Command{
- Title: "run file benchmarks",
- Command: CommandTest.ID(),
- Arguments: args,
- },
- })
return codeLens, nil
}
diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go
index 14afb8f..51f85d1 100644
--- a/internal/lsp/source/completion/completion.go
+++ b/internal/lsp/source/completion/completion.go
@@ -733,6 +733,10 @@
// (i.e. "golang.org/x/"). The user is meant to accept completion suggestions
// until they reach a complete import path.
func (c *completer) populateImportCompletions(ctx context.Context, searchImport *ast.ImportSpec) error {
+ if !strings.HasPrefix(searchImport.Path.Value, `"`) {
+ return nil
+ }
+
// deepSearch is not valuable for import completions.
c.deepState.enabled = false
@@ -1070,6 +1074,19 @@
// Is sel a qualified identifier?
if id, ok := sel.X.(*ast.Ident); ok {
if pkgName, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok {
+ var pkg source.Package
+ for _, imp := range c.pkg.Imports() {
+ if imp.PkgPath() == pkgName.Imported().Path() {
+ pkg = imp
+ }
+ }
+ // If the package is not imported, try searching for unimported
+ // completions.
+ if pkg == nil && c.opts.unimported {
+ if err := c.unimportedMembers(ctx, id); err != nil {
+ return err
+ }
+ }
candidates := c.packageMembers(pkgName.Imported(), stdScore, nil)
for _, cand := range candidates {
c.deepState.enqueue(cand)
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 77cb5c4..087c210 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -173,7 +173,10 @@
if fixedData == nil || fixedData[len(fixedData)-1] != '\n' {
fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure.
}
- edits := snapshot.View().Options().ComputeEdits(pgf.URI, left, string(fixedData))
+ edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, left, string(fixedData))
+ if err != nil {
+ return nil, err
+ }
return ToProtocolEdits(pgf.Mapper, edits)
}
@@ -270,7 +273,10 @@
_, done := event.Start(ctx, "source.computeTextEdits")
defer done()
- edits := snapshot.View().Options().ComputeEdits(pgf.URI, string(pgf.Src), formatted)
+ edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, string(pgf.Src), formatted)
+ if err != nil {
+ return nil, err
+ }
return ToProtocolEdits(pgf.Mapper, edits)
}
diff --git a/internal/lsp/source/format_test.go b/internal/lsp/source/format_test.go
index e0dfe15..52f8aae 100644
--- a/internal/lsp/source/format_test.go
+++ b/internal/lsp/source/format_test.go
@@ -33,7 +33,7 @@
} {
got := importPrefix([]byte(tt.input))
if got != tt.want {
- t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(tt.want, got))
+ t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, tt.want, got))
}
}
}
@@ -61,18 +61,21 @@
got := importPrefix([]byte(strings.ReplaceAll(tt.input, "\n", "\r\n")))
want := strings.ReplaceAll(tt.want, "\n", "\r\n")
if got != want {
- t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(want, got))
+ t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, want, got))
}
}
}
-func diffStr(want, got string) string {
+func diffStr(t *testing.T, want, got string) string {
if want == got {
return ""
}
// Add newlines to avoid newline messages in diff.
want += "\n"
got += "\n"
- d := myers.ComputeEdits("", want, got)
+ d, err := myers.ComputeEdits("", want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d))
}
diff --git a/internal/lsp/source/gc_annotations.go b/internal/lsp/source/gc_annotations.go
index 4aac541..776cbb0 100644
--- a/internal/lsp/source/gc_annotations.go
+++ b/internal/lsp/source/gc_annotations.go
@@ -19,6 +19,22 @@
"golang.org/x/tools/internal/span"
)
+type Annotation string
+
+const (
+ // Nil controls nil checks.
+ Nil Annotation = "nil"
+
+ // Escape controls diagnostics about escape choices.
+ Escape Annotation = "escape"
+
+ // Inline controls diagnostics about inlining choices.
+ Inline Annotation = "inline"
+
+ // Bounds controls bounds checking diagnostics.
+ Bounds Annotation = "bounds"
+)
+
func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, pkgDir span.URI) (map[VersionedFileIdentity][]*Diagnostic, error) {
outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid()))
@@ -113,7 +129,7 @@
if msg != "" {
msg = fmt.Sprintf("%s(%s)", msg, d.Message)
}
- if skipDiagnostic(msg, d.Source, options) {
+ if !showDiagnostic(msg, d.Source, options) {
continue
}
var related []RelatedInformation
@@ -138,24 +154,27 @@
return uri, diagnostics, nil
}
-// skipDiagnostic reports whether a given diagnostic should be shown to the end
+// showDiagnostic reports whether a given diagnostic should be shown to the end
// user, given the current options.
-func skipDiagnostic(msg, source string, o *Options) bool {
+func showDiagnostic(msg, source string, o *Options) bool {
if source != "go compiler" {
return false
}
+ if o.Annotations == nil {
+ return true
+ }
switch {
- case o.Annotations["noInline"]:
- return strings.HasPrefix(msg, "canInline") ||
- strings.HasPrefix(msg, "cannotInline") ||
- strings.HasPrefix(msg, "inlineCall")
- case o.Annotations["noEscape"]:
- return strings.HasPrefix(msg, "escape") || msg == "leak"
- case o.Annotations["noNilcheck"]:
- return strings.HasPrefix(msg, "nilcheck")
- case o.Annotations["noBounds"]:
- return strings.HasPrefix(msg, "isInBounds") ||
- strings.HasPrefix(msg, "isSliceInBounds")
+ case strings.HasPrefix(msg, "canInline") ||
+ strings.HasPrefix(msg, "cannotInline") ||
+ strings.HasPrefix(msg, "inlineCall"):
+ return o.Annotations[Inline]
+ case strings.HasPrefix(msg, "escape") || msg == "leak":
+ return o.Annotations[Escape]
+ case strings.HasPrefix(msg, "nilcheck"):
+ return o.Annotations[Nil]
+ case strings.HasPrefix(msg, "isInBounds") ||
+ strings.HasPrefix(msg, "isSliceInBounds"):
+ return o.Annotations[Bounds]
}
return false
}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 797c205..4e20893 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -32,6 +32,7 @@
"golang.org/x/tools/go/analysis/passes/lostcancel"
"golang.org/x/tools/go/analysis/passes/nilfunc"
"golang.org/x/tools/go/analysis/passes/printf"
+ "golang.org/x/tools/go/analysis/passes/shadow"
"golang.org/x/tools/go/analysis/passes/shift"
"golang.org/x/tools/go/analysis/passes/sortslice"
"golang.org/x/tools/go/analysis/passes/stdmethods"
@@ -63,8 +64,6 @@
defaultOptions *Options
)
-//go:generate go run golang.org/x/tools/internal/lsp/source/genapijson -output api_json.go
-
// DefaultOptions is the options that are used for Gopls execution independent
// of any externally provided configuration (LSP initialization, command
// invokation, etc.).
@@ -101,29 +100,43 @@
SupportedCommands: commands,
},
UserOptions: UserOptions{
- HoverKind: FullDocumentation,
- LinkTarget: "pkg.go.dev",
- Codelenses: map[string]bool{
- CommandGenerate.Name: true,
- CommandRegenerateCgo.Name: true,
- CommandTidy.Name: true,
- CommandToggleDetails.Name: false,
- CommandUpgradeDependency.Name: true,
- CommandVendor.Name: true,
+ BuildOptions: BuildOptions{
+ ExpandWorkspaceToModule: true,
+ ExperimentalPackageCacheKey: true,
},
- LinksInHover: true,
- ImportShortcut: Both,
- Matcher: Fuzzy,
- SymbolMatcher: SymbolFuzzy,
- SymbolStyle: DynamicSymbols,
- },
- DebuggingOptions: DebuggingOptions{
- CompletionBudget: 100 * time.Millisecond,
- },
- ExperimentalOptions: ExperimentalOptions{
- ExpandWorkspaceToModule: true,
- ExperimentalPackageCacheKey: true,
- ExperimentalDiagnosticsDelay: 250 * time.Millisecond,
+ UIOptions: UIOptions{
+ DiagnosticOptions: DiagnosticOptions{
+ ExperimentalDiagnosticsDelay: 250 * time.Millisecond,
+ Annotations: map[Annotation]bool{
+ Bounds: true,
+ Escape: true,
+ Inline: true,
+ Nil: true,
+ },
+ },
+ DocumentationOptions: DocumentationOptions{
+ HoverKind: FullDocumentation,
+ LinkTarget: "pkg.go.dev",
+ LinksInHover: true,
+ },
+ NavigationOptions: NavigationOptions{
+ ImportShortcut: Both,
+ SymbolMatcher: SymbolFuzzy,
+ SymbolStyle: DynamicSymbols,
+ },
+ CompletionOptions: CompletionOptions{
+ Matcher: Fuzzy,
+ CompletionBudget: 100 * time.Millisecond,
+ },
+ Codelenses: map[string]bool{
+ CommandGenerate.Name: true,
+ CommandRegenerateCgo.Name: true,
+ CommandTidy.Name: true,
+ CommandToggleDetails.Name: false,
+ CommandUpgradeDependency.Name: true,
+ CommandVendor.Name: true,
+ },
+ },
},
InternalOptions: InternalOptions{
LiteralCompletions: true,
@@ -152,8 +165,6 @@
ClientOptions
ServerOptions
UserOptions
- DebuggingOptions
- ExperimentalOptions
InternalOptions
Hooks
}
@@ -179,9 +190,7 @@
SupportedCommands []string
}
-// UserOptions holds custom Gopls configuration (not part of the LSP) that is
-// modified by the client.
-type UserOptions struct {
+type BuildOptions struct {
// BuildFlags is the set of flags passed on to the build system when invoked.
// It is applied to queries like `go list`, which is used when discovering files.
// The most common use is to set `-tags`.
@@ -190,85 +199,6 @@
// Env adds environment variables to external commands run by `gopls`, most notably `go list`.
Env map[string]string
- // HoverKind controls the information that appears in the hover text.
- // SingleLine and Structured are intended for use only by authors of editor plugins.
- HoverKind HoverKind
-
- // Placeholders enables placeholders for function parameters or struct fields in completion responses.
- UsePlaceholders bool
-
- // LinkTarget controls where documentation links go.
- // It might be one of:
- //
- // * `"godoc.org"`
- // * `"pkg.go.dev"`
- //
- // If company chooses to use its own `godoc.org`, its address can be used as well.
- LinkTarget string
-
- // Local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.
- // It should be the prefix of the import path whose imports should be grouped separately.
- Local string
-
- // Gofumpt indicates if we should run gofumpt formatting.
- Gofumpt bool
-
- // Analyses specify analyses that the user would like to enable or disable.
- // A map of the names of analysis passes that should be enabled/disabled.
- // A full list of analyzers that gopls uses can be found [here](analyzers.md)
- //
- // Example Usage:
- // ```json5
- // ...
- // "analyses": {
- // "unreachable": false, // Disable the unreachable analyzer.
- // "unusedparams": true // Enable the unusedparams analyzer.
- // }
- // ...
- // ```
- Analyses map[string]bool
-
- // Codelenses overrides the enabled/disabled state of code lenses. See the "Code Lenses"
- // section of settings.md for the list of supported lenses.
- //
- // Example Usage:
- // ```json5
- // "gopls": {
- // ...
- // "codelens": {
- // "generate": false, // Don't show the `go generate` lens.
- // "gc_details": true // Show a code lens toggling the display of gc's choices.
- // }
- // ...
- // }
- // ```
- Codelenses map[string]bool
-
- // LinksInHover toggles the presence of links to documentation in hover.
- LinksInHover bool
-
- // ImportShortcut specifies whether import statements should link to
- // documentation or go to definitions.
- ImportShortcut ImportShortcut
-
- // Matcher sets the algorithm that is used when calculating completion candidates.
- Matcher Matcher
-
- // SymbolMatcher sets the algorithm that is used when finding workspace symbols.
- SymbolMatcher SymbolMatcher
-
- // SymbolStyle controls how symbols are qualified in symbol responses.
- //
- // Example Usage:
- // ```json5
- // "gopls": {
- // ...
- // "symbolStyle": "dynamic",
- // ...
- // }
- // ```
- SymbolStyle SymbolStyle
-
// DirectoryFilters can be used to exclude unwanted directories from the
// workspace. By default, all directories are included. Filters are an
// operator, `+` to include and `-` to exclude, followed by a path prefix
@@ -281,6 +211,179 @@
// Include only project_a: `-` (exclude everything), `+project_a`
// Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`
DirectoryFilters []string
+
+ // ExpandWorkspaceToModule instructs `gopls` to adjust the scope of the
+ // workspace to find the best available module root. `gopls` first looks for
+ // a go.mod file in any parent directory of the workspace folder, expanding
+ // the scope to that directory if it exists. If no viable parent directory is
+ // found, gopls will check if there is exactly one child directory containing
+ // a go.mod file, narrowing the scope to that directory if it exists.
+ ExpandWorkspaceToModule bool `status:"experimental"`
+
+ // ExperimentalWorkspaceModule opts a user into the experimental support
+ // for multi-module workspaces.
+ ExperimentalWorkspaceModule bool `status:"experimental"`
+
+ // ExperimentalPackageCacheKey controls whether to use a coarser cache key
+ // for package type information to increase cache hits. This setting removes
+ // the user's environment, build flags, and working directory from the cache
+ // key, which should be a safe change as all relevant inputs into the type
+ // checking pass are already hashed into the key. This is temporarily guarded
+ // by an experiment because caching behavior is subtle and difficult to
+ // comprehensively test.
+ ExperimentalPackageCacheKey bool `status:"experimental"`
+
+ // AllowModfileModifications disables -mod=readonly, allowing imports from
+ // out-of-scope modules. This option will eventually be removed.
+ AllowModfileModifications bool `status:"experimental"`
+
+ // AllowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module
+ // downloads rather than requiring user action. This option will eventually
+ // be removed.
+ AllowImplicitNetworkAccess bool `status:"experimental"`
+}
+
+type UIOptions struct {
+ DocumentationOptions
+ CompletionOptions
+ NavigationOptions
+ DiagnosticOptions
+
+ // Codelenses overrides the enabled/disabled state of code lenses. See the
+ // "Code Lenses" section of the
+ // [Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md)
+ // for the list of supported lenses.
+ //
+ // Example Usage:
+ //
+ // ```json5
+ // "gopls": {
+ // ...
+ // "codelens": {
+ // "generate": false, // Don't show the `go generate` lens.
+ // "gc_details": true // Show a code lens toggling the display of gc's choices.
+ // }
+ // ...
+ // }
+ // ```
+ Codelenses map[string]bool
+
+ // SemanticTokens controls whether the LSP server will send
+ // semantic tokens to the client.
+ SemanticTokens bool `status:"experimental"`
+}
+
+type CompletionOptions struct {
+ // Placeholders enables placeholders for function parameters or struct
+ // fields in completion responses.
+ UsePlaceholders bool
+
+ // CompletionBudget is the soft latency goal for completion requests. Most
+ // requests finish in a couple milliseconds, but in some cases deep
+ // completions can take much longer. As we use up our budget we
+ // dynamically reduce the search scope to ensure we return timely
+ // results. Zero means unlimited.
+ CompletionBudget time.Duration `status:"debug"`
+
+ // Matcher sets the algorithm that is used when calculating completion
+ // candidates.
+ Matcher Matcher `status:"advanced"`
+}
+
+type DocumentationOptions struct {
+ // HoverKind controls the information that appears in the hover text.
+ // SingleLine and Structured are intended for use only by authors of editor plugins.
+ HoverKind HoverKind
+
+ // LinkTarget controls where documentation links go.
+ // It might be one of:
+ //
+ // * `"godoc.org"`
+ // * `"pkg.go.dev"`
+ //
+ // If company chooses to use its own `godoc.org`, its address can be used as well.
+ LinkTarget string
+
+ // LinksInHover toggles the presence of links to documentation in hover.
+ LinksInHover bool
+}
+
+type FormattingOptions struct {
+ // Local is the equivalent of the `goimports -local` flag, which puts
+ // imports beginning with this string after third-party packages. It should
+ // be the prefix of the import path whose imports should be grouped
+ // separately.
+ Local string
+
+ // Gofumpt indicates if we should run gofumpt formatting.
+ Gofumpt bool
+}
+
+type DiagnosticOptions struct {
+ // Analyses specify analyses that the user would like to enable or disable.
+ // A map of the names of analysis passes that should be enabled/disabled.
+ // A full list of analyzers that gopls uses can be found
+ // [here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).
+ //
+ // Example Usage:
+ //
+ // ```json5
+ // ...
+ // "analyses": {
+ // "unreachable": false, // Disable the unreachable analyzer.
+ // "unusedparams": true // Enable the unusedparams analyzer.
+ // }
+ // ...
+ // ```
+ Analyses map[string]bool
+
+ // Staticcheck enables additional analyses from staticcheck.io.
+ Staticcheck bool `status:"experimental"`
+
+ // Annotations specifies the various kinds of optimization diagnostics
+ // that should be reported by the gc_details command.
+ Annotations map[Annotation]bool `status:"experimental"`
+
+ // ExperimentalDiagnosticsDelay controls the amount of time that gopls waits
+ // after the most recent file modification before computing deep diagnostics.
+ // Simple diagnostics (parsing and type-checking) are always run immediately
+ // on recently modified packages.
+ //
+ // This option must be set to a valid duration string, for example `"250ms"`.
+ ExperimentalDiagnosticsDelay time.Duration `status:"experimental"`
+}
+
+type NavigationOptions struct {
+ // ImportShortcut specifies whether import statements should link to
+ // documentation or go to definitions.
+ ImportShortcut ImportShortcut
+
+ // SymbolMatcher sets the algorithm that is used when finding workspace symbols.
+ SymbolMatcher SymbolMatcher `status:"advanced"`
+
+ // SymbolStyle controls how symbols are qualified in symbol responses.
+ //
+ // Example Usage:
+ //
+ // ```json5
+ // "gopls": {
+ // ...
+ // "symbolStyle": "dynamic",
+ // ...
+ // }
+ // ```
+ SymbolStyle SymbolStyle `status:"advanced"`
+}
+
+// UserOptions holds custom Gopls configuration (not part of the LSP) that is
+// modified by the client.
+type UserOptions struct {
+ BuildOptions
+ UIOptions
+ FormattingOptions
+
+ // VerboseOutput enables additional debug logging.
+ VerboseOutput bool `status:"debug"`
}
// EnvSlice returns Env as a slice of k=v strings.
@@ -317,79 +420,6 @@
StaticcheckAnalyzers map[string]Analyzer
}
-// ExperimentalOptions defines configuration for features under active
-// development. WARNING: This configuration will be changed in the future. It
-// only exists while these features are under development.
-type ExperimentalOptions struct {
-
- // Annotations suppress various kinds of optimization diagnostics
- // that would be reported by the gc_details command.
- // * noNilcheck suppresses display of nilchecks.
- // * noEscape suppresses escape choices.
- // * noInline suppresses inlining choices.
- // * noBounds suppresses bounds checking diagnostics.
- Annotations map[string]bool
-
- // Staticcheck enables additional analyses from staticcheck.io.
- Staticcheck bool
-
- // SemanticTokens controls whether the LSP server will send
- // semantic tokens to the client.
- SemanticTokens bool
-
- // ExpandWorkspaceToModule instructs `gopls` to adjust the scope of the
- // workspace to find the best available module root. `gopls` first looks for
- // a go.mod file in any parent directory of the workspace folder, expanding
- // the scope to that directory if it exists. If no viable parent directory is
- // found, gopls will check if there is exactly one child directory containing
- // a go.mod file, narrowing the scope to that directory if it exists.
- ExpandWorkspaceToModule bool
-
- // ExperimentalWorkspaceModule opts a user into the experimental support
- // for multi-module workspaces.
- ExperimentalWorkspaceModule bool
-
- // ExperimentalDiagnosticsDelay controls the amount of time that gopls waits
- // after the most recent file modification before computing deep diagnostics.
- // Simple diagnostics (parsing and type-checking) are always run immediately
- // on recently modified packages.
- //
- // This option must be set to a valid duration string, for example `"250ms"`.
- ExperimentalDiagnosticsDelay time.Duration
-
- // ExperimentalPackageCacheKey controls whether to use a coarser cache key
- // for package type information to increase cache hits. This setting removes
- // the user's environment, build flags, and working directory from the cache
- // key, which should be a safe change as all relevant inputs into the type
- // checking pass are already hashed into the key. This is temporarily guarded
- // by an experiment because caching behavior is subtle and difficult to
- // comprehensively test.
- ExperimentalPackageCacheKey bool
-
- // AllowModfileModifications disables -mod=readonly, allowing imports from
- // out-of-scope modules. This option will eventually be removed.
- AllowModfileModifications bool
-
- // AllowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module
- // downloads rather than requiring user action. This option will eventually
- // be removed.
- AllowImplicitNetworkAccess bool
-}
-
-// DebuggingOptions should not affect the logical execution of Gopls, but may
-// be altered for debugging purposes.
-type DebuggingOptions struct {
- // VerboseOutput enables additional debug logging.
- VerboseOutput bool
-
- // CompletionBudget is the soft latency goal for completion requests. Most
- // requests finish in a couple milliseconds, but in some cases deep
- // completions can take much longer. As we use up our budget we
- // dynamically reduce the search scope to ensure we return timely
- // results. Zero means unlimited.
- CompletionBudget time.Duration
-}
-
// InternalOptions contains settings that are not intended for use by the
// average user. These may be settings used by tests or outdated settings that
// will soon be deprecated. Some of these settings may not even be configurable
@@ -541,8 +571,9 @@
options.enableAllExperiments()
}
}
+ seen := map[string]struct{}{}
for name, value := range opts {
- results = append(results, options.set(name, value))
+ results = append(results, options.set(name, value, seen))
}
// Finally, enable any experimental features that are specified in
// maps, which allows users to individually toggle them on or off.
@@ -582,15 +613,12 @@
o.SemanticMods = caps.TextDocument.SemanticTokens.TokenModifiers
// we don't need Requests, as we support full functionality
// we don't need Formats, as there is only one, for now
-
}
func (o *Options) Clone() *Options {
result := &Options{
- ClientOptions: o.ClientOptions,
- DebuggingOptions: o.DebuggingOptions,
- ExperimentalOptions: o.ExperimentalOptions,
- InternalOptions: o.InternalOptions,
+ ClientOptions: o.ClientOptions,
+ InternalOptions: o.InternalOptions,
Hooks: Hooks{
GoDiff: o.Hooks.GoDiff,
ComputeEdits: o.Hooks.ComputeEdits,
@@ -610,7 +638,6 @@
return dst
}
result.Analyses = copyStringMap(o.Analyses)
- result.Annotations = copyStringMap(o.Annotations)
result.Codelenses = copyStringMap(o.Codelenses)
copySlice := func(src []string) []string {
@@ -656,8 +683,17 @@
}
}
-func (o *Options) set(name string, value interface{}) OptionResult {
+func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult {
+ // Flatten the name in case we get options with a hierarchy.
+ split := strings.Split(name, ".")
+ name = split[len(split)-1]
+
result := OptionResult{Name: name, Value: value}
+ if _, ok := seen[name]; ok {
+ result.errorf("duplicate configuration for %s", name)
+ }
+ seen[name] = struct{}{}
+
switch name {
case "env":
menv, ok := value.(map[string]interface{})
@@ -665,6 +701,9 @@
result.errorf("invalid type %T, expect map", value)
break
}
+ if o.Env == nil {
+ o.Env = make(map[string]string)
+ }
for k, v := range menv {
o.Env[k] = fmt.Sprint(v)
}
@@ -759,16 +798,7 @@
result.setBoolMap(&o.Analyses)
case "annotations":
- result.setBoolMap(&o.Annotations)
- for k := range o.Annotations {
- switch k {
- case "noEscape", "noNilcheck", "noInline", "noBounds":
- continue
- default:
- result.Name += ":" + k // put mistake(s) in the message
- result.State = OptionUnexpected
- }
- }
+ result.setAnnotationMap(&o.Annotations)
case "codelenses", "codelens":
var lensOverrides map[string]bool
@@ -912,10 +942,55 @@
}
func (r *OptionResult) setBoolMap(bm *map[string]bool) {
+ m := r.asBoolMap()
+ *bm = m
+}
+
+func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) {
+ all := r.asBoolMap()
+ if all == nil {
+ return
+ }
+ // Default to everything enabled by default.
+ m := make(map[Annotation]bool)
+ for k, enabled := range all {
+ a, err := asOneOf(
+ k,
+ string(Nil),
+ string(Escape),
+ string(Inline),
+ string(Bounds),
+ )
+ if err != nil {
+ // In case of an error, process any legacy values.
+ switch k {
+ case "noEscape":
+ m[Escape] = false
+ r.errorf(`"noEscape" is deprecated, set "Escape: false" instead`)
+ case "noNilcheck":
+ m[Nil] = false
+ r.errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`)
+ case "noInline":
+ m[Inline] = false
+ r.errorf(`"noInline" is deprecated, set "Inline: false" instead`)
+ case "noBounds":
+ m[Bounds] = false
+ r.errorf(`"noBounds" is deprecated, set "Bounds: false" instead`)
+ default:
+ r.errorf(err.Error())
+ }
+ continue
+ }
+ m[Annotation(a)] = enabled
+ }
+ *bm = m
+}
+
+func (r *OptionResult) asBoolMap() map[string]bool {
all, ok := r.Value.(map[string]interface{})
if !ok {
r.errorf("invalid type %T for map[string]bool option", r.Value)
- return
+ return nil
}
m := make(map[string]bool)
for a, enabled := range all {
@@ -923,10 +998,10 @@
m[a] = enabled
} else {
r.errorf("invalid type %T for map key %q", enabled, a)
- return
+ return m
}
}
- *bm = m
+ return m
}
func (r *OptionResult) asString() (string, bool) {
@@ -943,14 +1018,21 @@
if !ok {
return "", false
}
- lower := strings.ToLower(s)
+ s, err := asOneOf(s, options...)
+ if err != nil {
+ r.errorf(err.Error())
+ }
+ return s, err == nil
+}
+
+func asOneOf(str string, options ...string) (string, error) {
+ lower := strings.ToLower(str)
for _, opt := range options {
if strings.ToLower(opt) == lower {
- return opt, true
+ return opt, nil
}
}
- r.errorf("invalid option %q for enum", r.Value)
- return "", false
+ return "", fmt.Errorf("invalid option %q for enum", str)
}
func (r *OptionResult) setString(s *string) {
@@ -1054,6 +1136,7 @@
atomicalign.Analyzer.Name: {Analyzer: atomicalign.Analyzer, Enabled: true},
deepequalerrors.Analyzer.Name: {Analyzer: deepequalerrors.Analyzer, Enabled: true},
fieldalignment.Analyzer.Name: {Analyzer: fieldalignment.Analyzer, Enabled: false},
+ shadow.Analyzer.Name: {Analyzer: shadow.Analyzer, Enabled: false},
sortslice.Analyzer.Name: {Analyzer: sortslice.Analyzer, Enabled: true},
testinggoroutine.Analyzer.Name: {Analyzer: testinggoroutine.Analyzer, Enabled: true},
unusedparams.Analyzer.Name: {Analyzer: unusedparams.Analyzer, Enabled: false},
@@ -1073,17 +1156,32 @@
}
type APIJSON struct {
- Options map[string][]*OptionJSON
- Commands []*CommandJSON
- Lenses []*LensJSON
+ Options map[string][]*OptionJSON
+ Commands []*CommandJSON
+ Lenses []*LensJSON
+ Analyzers []*AnalyzerJSON
}
type OptionJSON struct {
Name string
Type string
Doc string
+ EnumKeys EnumKeys
EnumValues []EnumValue
Default string
+ Status string
+ Hierarchy string
+}
+
+type EnumKeys struct {
+ ValueType string
+ Keys []EnumKey
+}
+
+type EnumKey struct {
+ Name string
+ Doc string
+ Default string
}
type EnumValue struct {
@@ -1102,3 +1200,9 @@
Title string
Doc string
}
+
+type AnalyzerJSON struct {
+ Name string
+ Doc string
+ Default bool
+}
diff --git a/internal/lsp/source/options_test.go b/internal/lsp/source/options_test.go
index 67c8c10..83cb795 100644
--- a/internal/lsp/source/options_test.go
+++ b/internal/lsp/source/options_test.go
@@ -60,11 +60,117 @@
return true // just confirm that we handle this setting
},
},
+ {
+ name: "hoverKind",
+ value: "FullDocumentation",
+ check: func(o Options) bool {
+ return o.HoverKind == FullDocumentation
+ },
+ },
+ {
+ name: "hoverKind",
+ value: "NoDocumentation",
+ check: func(o Options) bool {
+ return o.HoverKind == NoDocumentation
+ },
+ },
+ {
+ name: "hoverKind",
+ value: "SingleLine",
+ check: func(o Options) bool {
+ return o.HoverKind == SingleLine
+ },
+ },
+ {
+ name: "hoverKind",
+ value: "Structured",
+ check: func(o Options) bool {
+ return o.HoverKind == Structured
+ },
+ },
+ {
+ name: "ui.documentation.hoverKind",
+ value: "Structured",
+ check: func(o Options) bool {
+ return o.HoverKind == Structured
+ },
+ },
+ {
+ name: "matcher",
+ value: "Fuzzy",
+ check: func(o Options) bool {
+ return o.Matcher == Fuzzy
+ },
+ },
+ {
+ name: "matcher",
+ value: "CaseSensitive",
+ check: func(o Options) bool {
+ return o.Matcher == CaseSensitive
+ },
+ },
+ {
+ name: "matcher",
+ value: "CaseInsensitive",
+ check: func(o Options) bool {
+ return o.Matcher == CaseInsensitive
+ },
+ },
+ {
+ name: "env",
+ value: map[string]interface{}{"testing": "true"},
+ check: func(o Options) bool {
+ v, found := o.Env["testing"]
+ return found && v == "true"
+ },
+ },
+ {
+ name: "env",
+ value: []string{"invalid", "input"},
+ wantError: true,
+ check: func(o Options) bool {
+ return o.Env == nil
+ },
+ },
+ {
+ name: "directoryFilters",
+ value: []interface{}{"-node_modules", "+project_a"},
+ check: func(o Options) bool {
+ return len(o.DirectoryFilters) == 2
+ },
+ },
+ {
+ name: "directoryFilters",
+ value: []interface{}{"invalid"},
+ wantError: true,
+ check: func(o Options) bool {
+ return len(o.DirectoryFilters) == 0
+ },
+ },
+ {
+ name: "directoryFilters",
+ value: []string{"-invalid", "+type"},
+ wantError: true,
+ check: func(o Options) bool {
+ return len(o.DirectoryFilters) == 0
+ },
+ },
+ {
+ name: "annotations",
+ value: map[string]interface{}{
+ "Nil": false,
+ "noBounds": true,
+ },
+ wantError: true,
+ check: func(o Options) bool {
+ return !o.Annotations[Nil] && !o.Annotations[Bounds]
+ },
+ },
}
for _, test := range tests {
var opts Options
- result := opts.set(test.name, test.value)
+ result := opts.set(test.name, test.value, map[string]struct{}{})
if (result.Error != nil) != test.wantError {
t.Fatalf("Options.set(%q, %v): result.Error = %v, want error: %t", test.name, test.value, result.Error, test.wantError)
}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 169e3c3..bc670db 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -533,7 +533,10 @@
return []byte(got), nil
}))
if want != got {
- d := myers.ComputeEdits(spn.URI(), want, got)
+ d, err := myers.ComputeEdits(spn.URI(), want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
t.Errorf("import failed for %s: %s", spn.URI().Filename(), diff.ToUnified("want", "got", want, d))
}
}
@@ -578,7 +581,7 @@
return []byte(hover), nil
}))
if hover != expectHover {
- t.Errorf("hover for %s failed:\n%s", d.Src, tests.Diff(expectHover, hover))
+ t.Errorf("hover for %s failed:\n%s", d.Src, tests.Diff(t, expectHover, hover))
}
}
if !d.OnlyHover {
@@ -885,7 +888,7 @@
want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
- if diff := tests.Diff(want, got); diff != "" {
+ if diff := tests.Diff(t, want, got); diff != "" {
t.Error(diff)
}
}
@@ -917,7 +920,11 @@
Signatures: []protocol.SignatureInformation{*gotSignature},
ActiveParameter: float64(gotActiveParameter),
}
- if diff := tests.DiffSignatures(spn, want, got); diff != "" {
+ diff, err := tests.DiffSignatures(spn, want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff != "" {
t.Error(diff)
}
}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 97cf5c5..329c89a 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -147,10 +147,8 @@
// WorkspacePackages returns the snapshot's top-level packages.
WorkspacePackages(ctx context.Context) ([]Package, error)
- // WorkspaceLayoutError reports whether there might be any problems with
- // the user's workspace configuration, which would cause bad or incorrect
- // diagnostics.
- WorkspaceLayoutError(ctx context.Context) *CriticalError
+ // GetCriticalError returns any critical errors in the workspace.
+ GetCriticalError(ctx context.Context) *CriticalError
}
// PackageFilter sets how a package is filtered out from a set of packages
diff --git a/internal/lsp/testdata/callhierarchy/callhierarchy.go b/internal/lsp/testdata/callhierarchy/callhierarchy.go
index 95cb471..4f45a28 100644
--- a/internal/lsp/testdata/callhierarchy/callhierarchy.go
+++ b/internal/lsp/testdata/callhierarchy/callhierarchy.go
@@ -26,12 +26,14 @@
}
// D is exported to test incoming/outgoing calls across packages
-func D() { //@mark(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB)
+func D() { //@mark(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo)
e()
x()
F()
g()
outgoing.B()
+ foo := func() {} //@mark(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo)
+ foo()
}
func e() {} //@mark(hierarchyE, "e")
diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden
index a2bb473..4c35b25 100644
--- a/internal/lsp/testdata/summary.txt.golden
+++ b/internal/lsp/testdata/summary.txt.golden
@@ -1,5 +1,5 @@
-- summary --
-CallHierarchyCount = 1
+CallHierarchyCount = 2
CodeLensCount = 5
CompletionsCount = 258
CompletionSnippetCount = 88
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 7eaf9ac..bc44f36 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -880,7 +880,7 @@
}))
got := buf.String()
if want != got {
- t.Errorf("test summary does not match:\n%s", Diff(want, got))
+ t.Errorf("test summary does not match:\n%s", Diff(t, want, got))
}
}
@@ -1131,6 +1131,9 @@
}
func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) {
+ if data.CallHierarchy[src] == nil {
+ data.CallHierarchy[src] = &CallHierarchyResult{}
+ }
for _, call := range calls {
m, err := data.Mapper(call.URI())
if err != nil {
@@ -1141,19 +1144,11 @@
data.t.Fatal(err)
}
// we're only comparing protocol.range
- if data.CallHierarchy[src] != nil {
- data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls,
- protocol.CallHierarchyItem{
- URI: protocol.DocumentURI(call.URI()),
- Range: rng,
- })
- } else {
- data.CallHierarchy[src] = &CallHierarchyResult{
- OutgoingCalls: []protocol.CallHierarchyItem{
- {URI: protocol.DocumentURI(call.URI()), Range: rng},
- },
- }
- }
+ data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls,
+ protocol.CallHierarchyItem{
+ URI: protocol.DocumentURI(call.URI()),
+ Range: rng,
+ })
}
}
diff --git a/internal/lsp/tests/util.go b/internal/lsp/tests/util.go
index b46f159..94c948d 100644
--- a/internal/lsp/tests/util.go
+++ b/internal/lsp/tests/util.go
@@ -237,25 +237,28 @@
return msg.String()
}
-func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string {
+func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, error) {
decorate := func(f string, args ...interface{}) string {
return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
}
if len(got.Signatures) != 1 {
- return decorate("wanted 1 signature, got %d", len(got.Signatures))
+ return decorate("wanted 1 signature, got %d", len(got.Signatures)), nil
}
if got.ActiveSignature != 0 {
- return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature))
+ return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)), nil
}
if want.ActiveParameter != got.ActiveParameter {
- return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter))
+ return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)), nil
}
g := got.Signatures[0]
w := want.Signatures[0]
if w.Label != g.Label {
wLabel := w.Label + "\n"
- d := myers.ComputeEdits("", wLabel, g.Label+"\n")
- return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d))
+ d, err := myers.ComputeEdits("", wLabel, g.Label+"\n")
+ if err != nil {
+ return "", err
+ }
+ return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d)), err
}
var paramParts []string
for _, p := range g.Parameters {
@@ -263,9 +266,9 @@
}
paramsStr := strings.Join(paramParts, ", ")
if !strings.Contains(g.Label, paramsStr) {
- return decorate("expected signature %q to contain params %q", g.Label, paramsStr)
+ return decorate("expected signature %q to contain params %q", g.Label, paramsStr), nil
}
- return ""
+ return "", nil
}
// DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies
@@ -535,13 +538,16 @@
}
}
-func Diff(want, got string) string {
+func Diff(t *testing.T, want, got string) string {
if want == got {
return ""
}
// Add newlines to avoid newline messages in diff.
want += "\n"
got += "\n"
- d := myers.ComputeEdits("", want, got)
+ d, err := myers.ComputeEdits("", want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d))
}