all: merge master (9c97539) into gopls-release-branch.0.12
Also put back the x/tools replace directive in gopls/go.mod.
For golang/go#59818
Merge List:
+ 2023-05-24 9c97539a2 gopls/internal/lsp/cache: remove nested module warning
+ 2023-05-24 d44a094d8 gopls/internal/lsp/cmd: add a stats -anon flag to show anonymous data
+ 2023-05-24 e106694df gopls/internal/lsp: bundle certain quick-fixes with their diagnostic
+ 2023-05-24 5dc3f7433 gopls/internal/lsp/filecache: reenable memory cache layer
+ 2023-05-24 7e146a6c6 gopls/internal/lsp/cmd: simplify connection type
+ 2023-05-24 1e6066861 gopls/internal/regtest/workspace: unskip duplicate modules test
+ 2023-05-23 5ce721db5 gopls/doc: Fix broken links
+ 2023-05-23 7a03febee gopls/internal/lsp/cmd: remove vestiges of debugging golang/go#59475
+ 2023-05-22 a70f2bc21 gopls/internal/regtest/misc: update and unskip TestHoverIntLiteral
+ 2023-05-22 6997d196f gopls/internal/regtest/misc: unskip TestMajorOptionsChange
+ 2023-05-22 ec543c5a2 gopls/internal/lsp/cache: fix crash in Session.updateViewLocked
+ 2023-05-22 a12ee94f7 gopls/internal/regtest/misc: update some unilaterally skipped tests
+ 2023-05-22 5ff5cbb00 gopls: deprecate support for Go 1.16 and 1.17, update warnings
+ 2023-05-22 e6fd7f4c0 gopls/internal/lsp/cache: limit module scan to 100K files
+ 2023-05-22 9ca66ba88 gopls/internal/lsp/regtest: delete TestWatchReplaceTargets
+ 2023-05-22 edbfdbebf gopls/internal/lsp/source/completion: (unimported) add placeholders
+ 2023-05-22 3a5dbf351 gopls: add a new "subdirWatchPatterns" setting
+ 2023-05-22 3c0255176 internal/typesinternal: remove NewObjectpathFunc
+ 2023-05-20 07293620c present: reformat doc comment for lack of inline code
+ 2023-05-20 d4e66bd9a go/ssa: TestStdlib: disable check that function names are distinct
+ 2023-05-20 738ea2bdc go/ssa: use core type for field accesses
+ 2023-05-19 2ec4299f3 gopls/internal/lsp: split file-watching glob patterns
+ 2023-05-19 43b02eab0 gopls/internal/lsp/cache: only delete the most relevant mod tidy handle
+ 2023-05-19 5919673c9 internal/lsp/filecache: eliminate 'kind' directories
+ 2023-05-19 a5ef6c3eb gopls/internal/lsp: keep track of overlays on the files map
+ 2023-05-19 d7f4359f8 gopls/internal/lsp/mod: optimizations for mod tidy diagnostics
+ 2023-05-19 2eb726b88 gopls/internal/lsp/filecache: touch only files older than 1h
+ 2023-05-19 b742cb9a5 gopls/internal/regtest/bench: add a benchmark for diagnosing saves
+ 2023-05-19 4d66324ee gopls/internal/lsp/cache: tweak error message
+ 2023-05-19 e46df400e gopls/internal/lsp/filecache: delayed tweaks from code review
+ 2023-05-19 3df69b827 gopls/internal/lsp/debug: remove memory monitoring
+ 2023-05-19 a069704d0 gopls/internal/lsp/filecache: avoid flock
+ 2023-05-18 3d53c2d20 gopls/internal/lsp/cache: fix race in adhoc reloading
+ 2023-05-17 8b4b27bce go/analysis/passes/slog: fix Group kv offset
+ 2023-05-17 242e5ed73 cover: eliminate an unnecessary fsync in TestParseProfiles
+ 2023-05-17 651d951bb go/ssa: fix typo in package docs
Change-Id: Ie57d55ba8f3ae9bcf868db08088f68955b5e4856
diff --git a/cover/profile_test.go b/cover/profile_test.go
index 3cecdac..925b397 100644
--- a/cover/profile_test.go
+++ b/cover/profile_test.go
@@ -6,8 +6,8 @@
import (
"fmt"
- "io/ioutil"
"os"
+ "path/filepath"
"reflect"
"testing"
)
@@ -208,26 +208,12 @@
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- f, err := ioutil.TempFile("", "")
- if err != nil {
- t.Fatalf("Failed to create a temp file: %v.", err)
- }
- defer func() {
- f.Close()
- os.Remove(f.Name())
- }()
- n, err := f.WriteString(tc.input)
- if err != nil {
- t.Fatalf("Failed to write to temp file: %v", err)
- }
- if n < len(tc.input) {
- t.Fatalf("Didn't write enough bytes to temp file (wrote %d, expected %d).", n, len(tc.input))
- }
- if err := f.Sync(); err != nil {
- t.Fatalf("Failed to sync temp file: %v", err)
+ fname := filepath.Join(t.TempDir(), "test.cov")
+ if err := os.WriteFile(fname, []byte(tc.input), 0644); err != nil {
+ t.Fatal(err)
}
- result, err := ParseProfiles(f.Name())
+ result, err := ParseProfiles(fname)
if err != nil {
if !tc.expectErr {
t.Errorf("Unexpected error: %v", err)
diff --git a/go/analysis/passes/slog/slog.go b/go/analysis/passes/slog/slog.go
index 874ebec..8429eab 100644
--- a/go/analysis/passes/slog/slog.go
+++ b/go/analysis/passes/slog/slog.go
@@ -204,7 +204,7 @@
"WarnCtx": 2,
"ErrorCtx": 2,
"Log": 3,
- "Group": 0,
+ "Group": 1,
},
"Logger": map[string]int{
"Debug": 1,
diff --git a/go/analysis/passes/slog/testdata/src/a/a.go b/go/analysis/passes/slog/testdata/src/a/a.go
index a13aac7..aa408d0 100644
--- a/go/analysis/passes/slog/testdata/src/a/a.go
+++ b/go/analysis/passes/slog/testdata/src/a/a.go
@@ -143,7 +143,8 @@
r.Add(1, 2) // want `slog.Record.Add arg "1" should be a string or a slog.Attr`
- _ = slog.Group("a", 1, 2, 3) // want `slog.Group arg "2" should be a string or a slog.Attr`
+ _ = slog.Group("key", "a", 1, "b", 2)
+ _ = slog.Group("key", "a", 1, 2, 3) // want `slog.Group arg "2" should be a string or a slog.Attr`
}
diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go
index 77de326..c86da0c 100644
--- a/go/ssa/builder_generic_test.go
+++ b/go/ssa/builder_generic_test.go
@@ -685,7 +685,7 @@
//@ instrs("f12", "*ssa.MakeMap", "make map[P]bool 1:int")
func f12[T any, P *struct{f T}](x T) map[P]bool { return map[P]bool{{}: true} }
- //@ instrs("f13", "&v[0:int]")
+ //@ instrs("f13", "*ssa.IndexAddr", "&v[0:int]")
//@ instrs("f13", "*ssa.Store", "*t0 = 7:int", "*v = *new(A):A")
func f13[A [3]int, PA *A](v PA) {
*v = A{7}
diff --git a/go/ssa/doc.go b/go/ssa/doc.go
index afda476..a687de4 100644
--- a/go/ssa/doc.go
+++ b/go/ssa/doc.go
@@ -66,7 +66,6 @@
// *FieldAddr ✔ ✔
// *FreeVar ✔
// *Function ✔ ✔ (func)
-// *GenericConvert ✔ ✔
// *Global ✔ ✔ (var)
// *Go ✔
// *If ✔
@@ -80,6 +79,7 @@
// *MakeMap ✔ ✔
// *MakeSlice ✔ ✔
// *MapUpdate ✔
+// *MultiConvert ✔ ✔
// *NamedConst ✔ (const)
// *Next ✔ ✔
// *Panic ✔
diff --git a/go/ssa/emit.go b/go/ssa/emit.go
index 80e30b6..fe2f6f0 100644
--- a/go/ssa/emit.go
+++ b/go/ssa/emit.go
@@ -476,7 +476,7 @@
// value of a field.
func emitImplicitSelections(f *Function, v Value, indices []int, pos token.Pos) Value {
for _, index := range indices {
- if st, vptr := deptr(v.Type()); vptr {
+ if st, vptr := deref(v.Type()); vptr {
fld := fieldOf(st, index)
instr := &FieldAddr{
X: v,
@@ -486,7 +486,7 @@
instr.setType(types.NewPointer(fld.Type()))
v = f.emit(instr)
// Load the field's value iff indirectly embedded.
- if _, fldptr := deptr(fld.Type()); fldptr {
+ if _, fldptr := deref(fld.Type()); fldptr {
v = emitLoad(f, v)
}
} else {
@@ -510,7 +510,7 @@
// field's value.
// Ident id is used for position and debug info.
func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast.Ident) Value {
- if st, vptr := deptr(v.Type()); vptr {
+ if st, vptr := deref(v.Type()); vptr {
fld := fieldOf(st, index)
instr := &FieldAddr{
X: v,
diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go
index eeb9681..313146d 100644
--- a/go/ssa/ssa.go
+++ b/go/ssa/ssa.go
@@ -865,7 +865,7 @@
type FieldAddr struct {
register
X Value // *struct
- Field int // field is typeparams.CoreType(X.Type().Underlying().(*types.Pointer).Elem()).(*types.Struct).Field(Field)
+ Field int // index into CoreType(CoreType(X.Type()).(*types.Pointer).Elem()).(*types.Struct).Fields
}
// The Field instruction yields the Field of struct X.
@@ -884,7 +884,7 @@
type Field struct {
register
X Value // struct
- Field int // index into typeparams.CoreType(X.Type()).(*types.Struct).Fields
+ Field int // index into CoreType(X.Type()).(*types.Struct).Fields
}
// The IndexAddr instruction yields the address of the element at
diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go
index 8b9f423..11782f7 100644
--- a/go/ssa/stdlib_test.go
+++ b/go/ssa/stdlib_test.go
@@ -36,7 +36,7 @@
func TestStdlib(t *testing.T) {
if testing.Short() {
- t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)")
+ t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)") // ~5s
}
testenv.NeedsTool(t, "go")
@@ -81,20 +81,33 @@
allFuncs := ssautil.AllFunctions(prog)
- // Check that all non-synthetic functions have distinct names.
- // Synthetic wrappers for exported methods should be distinct too,
- // except for unexported ones (explained at (*Function).RelString).
- byName := make(map[string]*ssa.Function)
- for fn := range allFuncs {
- if fn.Synthetic == "" || ast.IsExported(fn.Name()) {
- str := fn.String()
- prev := byName[str]
- byName[str] = fn
- if prev != nil {
- t.Errorf("%s: duplicate function named %s",
- prog.Fset.Position(fn.Pos()), str)
- t.Errorf("%s: (previously defined here)",
- prog.Fset.Position(prev.Pos()))
+ // The assertion below is not valid if the program contains
+ // variants of the same package, such as the test variants
+ // (e.g. package p as compiled for test executable x) obtained
+ // when cfg.Tests=true. Profile-guided optimization may
+ // lead to similar variation for non-test executables.
+ //
+ // Ideally, the test would assert that all functions within
+ // each executable (more generally: within any singly rooted
+ // transitively closed subgraph of the import graph) have
+ // distinct names, but that isn't so easy to compute efficiently.
+ // Disabling for now.
+ if false {
+ // Check that all non-synthetic functions have distinct names.
+ // Synthetic wrappers for exported methods should be distinct too,
+ // except for unexported ones (explained at (*Function).RelString).
+ byName := make(map[string]*ssa.Function)
+ for fn := range allFuncs {
+ if fn.Synthetic == "" || ast.IsExported(fn.Name()) {
+ str := fn.String()
+ prev := byName[str]
+ byName[str] = fn
+ if prev != nil {
+ t.Errorf("%s: duplicate function named %s",
+ prog.Fset.Position(fn.Pos()), str)
+ t.Errorf("%s: (previously defined here)",
+ prog.Fset.Position(prev.Pos()))
+ }
}
}
}
diff --git a/gopls/README.md b/gopls/README.md
index 56d1592..396f86c 100644
--- a/gopls/README.md
+++ b/gopls/README.md
@@ -93,6 +93,7 @@
| ----------- | --------------------------------------------------- |
| Go 1.12 | [gopls@v0.7.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.7.5) |
| Go 1.15 | [gopls@v0.9.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.9.5) |
+| Go 1.17 | [gopls@v0.11.0](https://github.com/golang/tools/releases/tag/gopls%2Fv0.11.0) |
Our extended support is enforced via [continuous integration with older Go
versions](doc/contributing.md#ci). This legacy Go CI may not block releases:
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 8fe677b..b259f63 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -267,6 +267,9 @@
"URI": string,
// The module path to remove.
"ModulePath": string,
+ // 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.
"OnlyDiagnostic": bool,
}
```
diff --git a/gopls/doc/design/implementation.md b/gopls/doc/design/implementation.md
index 859ec1c..e9b915b 100644
--- a/gopls/doc/design/implementation.md
+++ b/gopls/doc/design/implementation.md
@@ -37,12 +37,12 @@
[gopls]: https://github.com/golang/tools/tree/master/gopls
[internal/jsonrpc2]: https://github.com/golang/tools/tree/master/internal/jsonrpc2
-[internal/lsp]: https://github.com/golang/tools/tree/master/internal/lsp
-[internal/lsp/cache]: https://github.com/golang/tools/tree/master/internal/lsp/cache
-[internal/lsp/cmd]: https://github.com/golang/tools/tree/master/internal/lsp/cmd
-[internal/lsp/debug]: https://github.com/golang/tools/tree/master/internal/lsp/debug
-[internal/lsp/protocol]: https://github.com/golang/tools/tree/master/internal/lsp/protocol
-[internal/lsp/source]: https://github.com/golang/tools/tree/master/internal/lsp/source
+[internal/lsp]: https://github.com/golang/tools/tree/master/gopls/internal/lsp
+[internal/lsp/cache]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/cache
+[internal/lsp/cmd]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/cmd
+[internal/lsp/debug]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/debug
+[internal/lsp/protocol]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/protocol
+[internal/lsp/source]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/source
[internal/memoize]: https://github.com/golang/tools/tree/master/internal/memoize
-[internal/span]: https://github.com/golang/tools/tree/master/internal/span
+[internal/span]: https://github.com/golang/tools/tree/master/gopls/internal/span
[x/tools]: https://github.com/golang/tools
diff --git a/gopls/go.mod b/gopls/go.mod
index 5f64a62..ca3f9bf2 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -25,3 +25,5 @@
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect
)
+
+replace golang.org/x/tools => ../
diff --git a/gopls/go.sum b/gopls/go.sum
index fe1d7bb..f6308a7 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -42,86 +42,42 @@
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
-golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
-golang.org/x/tools v0.4.1-0.20221217013628-b4dfc36097e2/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.9.2-0.20230516204147-76f78597112f h1:sbeNk1oZVYGPodwxlh++zvjWdh/C9nWD3lkPTLPkzDU=
-golang.org/x/tools v0.9.2-0.20230516204147-76f78597112f/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815 h1:A9kONVi4+AnuOr1dopsibH6hLi1Huy54cbeJxnq4vmU=
golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815/go.mod h1:XJiVExZgoZfrrxoTeVsFYrSSk1snhfpOEC95JL+A4T0=
-golang.org/x/xerrors v0.0.0-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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/gopls/internal/bug/bug.go b/gopls/internal/bug/bug.go
index 1bf7d30..f72948b 100644
--- a/gopls/internal/bug/bug.go
+++ b/gopls/internal/bug/bug.go
@@ -17,6 +17,7 @@
"runtime/debug"
"sort"
"sync"
+ "time"
)
// PanicOnBugs controls whether to panic when bugs are reported.
@@ -32,12 +33,15 @@
// A Bug represents an unexpected event or broken invariant. They are used for
// capturing metadata that helps us understand the event.
+//
+// Bugs are JSON-serializable.
type Bug struct {
- File string // file containing the call to bug.Report
- Line int // line containing the call to bug.Report
- Description string // description of the bug
- Key string // key identifying the bug (file:line if available)
- Stack string // call stack
+ File string // file containing the call to bug.Report
+ Line int // line containing the call to bug.Report
+ Description string // description of the bug
+ Key string // key identifying the bug (file:line if available)
+ Stack string // call stack
+ AtTime time.Time // time the bug was reported
}
// Reportf reports a formatted bug message.
@@ -77,6 +81,7 @@
Description: description,
Key: key,
Stack: string(debug.Stack()),
+ AtTime: time.Now(),
}
mu.Lock()
diff --git a/gopls/internal/bug/bug_test.go b/gopls/internal/bug/bug_test.go
index 2e36221..8ca2aa5 100644
--- a/gopls/internal/bug/bug_test.go
+++ b/gopls/internal/bug/bug_test.go
@@ -5,8 +5,12 @@
package bug
import (
+ "encoding/json"
"fmt"
"testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
)
func resetForTesting() {
@@ -62,3 +66,26 @@
t.Errorf("got %q, want %q", got, want)
}
}
+
+func TestBugJSON(t *testing.T) {
+ b1 := Bug{
+ File: "foo.go",
+ Line: 1,
+ Description: "a bug",
+ Key: "foo.go:1",
+ Stack: "<stack>",
+ AtTime: time.Now(),
+ }
+
+ data, err := json.Marshal(b1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var b2 Bug
+ if err := json.Unmarshal(data, &b2); err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(b1, b2); diff != "" {
+ t.Errorf("bugs differ after JSON Marshal/Unmarshal (-b1 +b2):\n%s", diff)
+ }
+}
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index 83ea177..6631270 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -1190,7 +1190,7 @@
relatedInformation: s.view.Options().RelatedInformationSupported,
linkTarget: s.view.Options().LinkTarget,
- moduleMode: s.moduleMode(),
+ moduleMode: s.view.moduleMode(),
}, nil
}
@@ -1542,14 +1542,18 @@
if err != nil {
return nil, err
}
- errors = append(errors, &source.Diagnostic{
+ diag := &source.Diagnostic{
URI: imp.cgf.URI,
Range: rng,
Severity: protocol.SeverityError,
Source: source.TypeError,
Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err),
SuggestedFixes: fixes,
- })
+ }
+ if !source.BundleQuickFixes(diag) {
+ bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message)
+ }
+ errors = append(errors, diag)
}
}
}
@@ -1585,14 +1589,18 @@
if err != nil {
return nil, err
}
- errors = append(errors, &source.Diagnostic{
+ diag := &source.Diagnostic{
URI: pm.URI,
Range: rng,
Severity: protocol.SeverityError,
Source: source.TypeError,
Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err),
SuggestedFixes: fixes,
- })
+ }
+ if !source.BundleQuickFixes(diag) {
+ bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message)
+ }
+ errors = append(errors, diag)
break
}
}
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 111b074..939d084 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -378,53 +378,6 @@
return fmt.Errorf(msg), s.applyCriticalErrorToFiles(ctx, msg, openFiles)
}
- // If the user has one active go.mod file, they may still be editing files
- // in nested modules. Check the module of each open file and add warnings
- // that the nested module must be opened as a workspace folder.
- if len(s.workspaceModFiles) == 1 {
- // Get the active root go.mod file to compare against.
- var rootMod string
- for uri := range s.workspaceModFiles {
- rootMod = uri.Filename()
- }
- rootDir := filepath.Dir(rootMod)
- nestedModules := make(map[string][]*Overlay)
- for _, fh := range openFiles {
- mod, err := findRootPattern(ctx, filepath.Dir(fh.URI().Filename()), "go.mod", s)
- if err != nil {
- if ctx.Err() != nil {
- return ctx.Err(), nil
- }
- continue
- }
- if mod == "" {
- continue
- }
- if mod != rootMod && source.InDir(rootDir, mod) {
- modDir := filepath.Dir(mod)
- nestedModules[modDir] = append(nestedModules[modDir], fh)
- }
- }
- var multiModuleMsg string
- if s.view.goversion >= 18 {
- multiModuleMsg = `To work on multiple modules at once, please use a go.work file.
-See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.`
- } else {
- multiModuleMsg = `To work on multiple modules at once, please upgrade to Go 1.18 and use a go.work file.
-See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.`
- }
- // Add a diagnostic to each file in a nested module to mark it as
- // "orphaned". Don't show a general diagnostic in the progress bar,
- // because the user may still want to edit a file in a nested module.
- var srcDiags []*source.Diagnostic
- for modDir, files := range nestedModules {
- msg := fmt.Sprintf("This file is in %s, which is a nested module in the %s module.\n%s", modDir, rootMod, multiModuleMsg)
- srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, files)...)
- }
- if len(srcDiags) != 0 {
- return fmt.Errorf("You have opened a nested module.\n%s", multiModuleMsg), srcDiags
- }
- }
return nil, nil
}
diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go
index 0ad4ac9..533c339 100644
--- a/gopls/internal/lsp/cache/maps.go
+++ b/gopls/internal/lsp/cache/maps.go
@@ -15,7 +15,8 @@
// TODO(euroelessar): Use generics once support for go1.17 is dropped.
type filesMap struct {
- impl *persistent.Map
+ impl *persistent.Map
+ overlayMap map[span.URI]*Overlay // the subset that are overlays
}
// uriLessInterface is the < relation for "any" values containing span.URIs.
@@ -25,13 +26,19 @@
func newFilesMap() filesMap {
return filesMap{
- impl: persistent.NewMap(uriLessInterface),
+ impl: persistent.NewMap(uriLessInterface),
+ overlayMap: make(map[span.URI]*Overlay),
}
}
func (m filesMap) Clone() filesMap {
+ overlays := make(map[span.URI]*Overlay, len(m.overlayMap))
+ for k, v := range m.overlayMap {
+ overlays[k] = v
+ }
return filesMap{
- impl: m.impl.Clone(),
+ impl: m.impl.Clone(),
+ overlayMap: overlays,
}
}
@@ -55,10 +62,30 @@
func (m filesMap) Set(key span.URI, value source.FileHandle) {
m.impl.Set(key, value, nil)
+
+ if o, ok := value.(*Overlay); ok {
+ m.overlayMap[key] = o
+ } else {
+ // Setting a non-overlay must delete the corresponding overlay, to preserve
+ // the accuracy of the overlay set.
+ delete(m.overlayMap, key)
+ }
}
-func (m filesMap) Delete(key span.URI) {
+func (m *filesMap) Delete(key span.URI) {
m.impl.Delete(key)
+ delete(m.overlayMap, key)
+}
+
+// overlays returns a new unordered array of overlay files.
+func (m filesMap) overlays() []*Overlay {
+ // In practice we will always have at least one overlay, so there is no need
+ // to optimize for the len=0 case by returning a nil slice.
+ overlays := make([]*Overlay, 0, len(m.overlayMap))
+ for _, o := range m.overlayMap {
+ overlays = append(overlays, o)
+ }
+ return overlays
}
func packageIDLessInterface(x, y interface{}) bool {
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index b5e2dea..8dd555d 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -183,7 +183,33 @@
// go.mod file. The fixes will be for the go.mod file, but the
// diagnostics should also appear in both the go.mod file and the import
// statements in the Go files in which the dependencies are used.
+ // Finally, add errors for any unused dependencies.
+ if len(missing) > 0 {
+ missingModuleDiagnostics, err := missingModuleDiagnostics(ctx, snapshot, pm, ideal, missing)
+ if err != nil {
+ return nil, err
+ }
+ diagnostics = append(diagnostics, missingModuleDiagnostics...)
+ }
+
+ // Opt: if this is the only diagnostic, we can avoid textual edits and just
+ // run the Go command.
+ //
+ // See also the documentation for command.RemoveDependencyArgs.OnlyDiagnostic.
+ onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
+ for _, req := range unused {
+ srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
+ if err != nil {
+ return nil, err
+ }
+ diagnostics = append(diagnostics, srcErr)
+ }
+ return diagnostics, nil
+}
+
+func missingModuleDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File, missing map[string]*modfile.Require) ([]*source.Diagnostic, error) {
missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
+ var diagnostics []*source.Diagnostic
for _, req := range missing {
srcDiag, err := missingModuleDiagnostic(pm, req)
if err != nil {
@@ -290,15 +316,6 @@
}
}
}
- // Finally, add errors for any unused dependencies.
- onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
- for _, req := range unused {
- srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
- if err != nil {
- return nil, err
- }
- diagnostics = append(diagnostics, srcErr)
- }
return diagnostics, nil
}
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index eaad67c..8eae64e 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -109,13 +109,14 @@
// TODO(rfindley): clarify that createView can never be cancelled (with the
// possible exception of server shutdown).
+// On success, the caller becomes responsible for calling the release function once.
func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) {
index := atomic.AddInt64(&viewIndex, 1)
// Get immutable workspace information.
info, err := s.getWorkspaceInformation(ctx, folder, options)
if err != nil {
- return nil, nil, func() {}, err
+ return nil, nil, nil, err
}
gowork, _ := info.GOWORK()
@@ -327,6 +328,15 @@
}
v, snapshot, release, err := s.createView(ctx, view.name, view.folder, options, seqID)
+ if err != nil {
+ // we have dropped the old view, but could not create the new one
+ // this should not happen and is very bad, but we still need to clean
+ // up the view array if it happens
+ s.views = removeElement(s.views, i)
+ return nil, err
+ }
+ defer release()
+
// The new snapshot has lost the history of the previous view. As a result,
// it may not see open files that aren't in its build configuration (as it
// would have done via didOpen notifications). This can lead to inconsistent
@@ -336,15 +346,7 @@
for _, o := range v.fs.Overlays() {
_, _ = snapshot.ReadFile(ctx, o.URI())
}
- release()
- if err != nil {
- // we have dropped the old view, but could not create the new one
- // this should not happen and is very bad, but we still need to clean
- // up the view array if it happens
- s.views = removeElement(s.views, i)
- return nil, err
- }
// substitute the new view into the array where the old view was
s.views[i] = v
return v, nil
@@ -596,20 +598,17 @@
for _, c := range changes {
if !knownDirs.Contains(c.URI) {
result = append(result, c)
- continue
+ } else {
+ for uri := range knownFilesInDir(ctx, snapshots, c.URI) {
+ result = append(result, source.FileModification{
+ URI: uri,
+ Action: c.Action,
+ LanguageID: "",
+ OnDisk: c.OnDisk,
+ // changes to directories cannot include text or versions
+ })
+ }
}
- affectedFiles := knownFilesInDir(ctx, snapshots, c.URI)
- var fileChanges []source.FileModification
- for uri := range affectedFiles {
- fileChanges = append(fileChanges, source.FileModification{
- URI: uri,
- Action: c.Action,
- LanguageID: "",
- OnDisk: c.OnDisk,
- // changes to directories cannot include text or versions
- })
- }
- result = append(result, fileChanges...)
}
return result
}
@@ -738,9 +737,10 @@
return nil
}
-// FileWatchingGlobPatterns returns glob patterns to watch every directory
-// known by the view. For views within a module, this is the module root,
-// any directory in the module root, and any replace targets.
+// FileWatchingGlobPatterns returns a new set of glob patterns to
+// watch every directory known by the view. For views within a module,
+// this is the module root, any directory in the module root, and any
+// replace targets.
func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
s.viewMu.Lock()
defer s.viewMu.Unlock()
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 7988a72..de9524b 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -159,10 +159,10 @@
modWhyHandles *persistent.Map // from span.URI to *memoize.Promise[modWhyResult]
modVulnHandles *persistent.Map // from span.URI to *memoize.Promise[modVulnResult]
- // knownSubdirs is the set of subdirectories in the workspace, used to
- // create glob patterns for file watching.
- knownSubdirs knownDirsSet
- knownSubdirsPatternCache string
+ // knownSubdirs is the set of subdirectory URIs in the workspace,
+ // used to create glob patterns for file watching.
+ knownSubdirs knownDirsSet
+ knownSubdirsCache map[string]struct{} // memo of knownSubdirs as a set of filenames
// unprocessedSubdirChanges are any changes that might affect the set of
// subdirectories in the workspace. They are not reflected to knownSubdirs
// during the snapshot cloning step as it can slow down cloning.
@@ -327,47 +327,19 @@
if s.view.hasGopackagesDriver {
return true
}
+
// Check if the user is working within a module or if we have found
// multiple modules in the workspace.
if len(s.workspaceModFiles) > 0 {
return true
}
- // The user may have a multiple directories in their GOPATH.
- // Check if the workspace is within any of them.
+
// TODO(rfindley): this should probably be subject to "if GO111MODULES = off {...}".
- for _, gp := range filepath.SplitList(s.view.gopath) {
- if source.InDir(filepath.Join(gp, "src"), s.view.folder.Filename()) {
- return true
- }
- }
- return false
-}
-
-// moduleMode reports whether the current snapshot uses Go modules.
-//
-// From https://go.dev/ref/mod, module mode is active if either of the
-// following hold:
-// - GO111MODULE=on
-// - GO111MODULE=auto and we are inside a module or have a GOWORK value.
-//
-// Additionally, this method returns false if GOPACKAGESDRIVER is set.
-//
-// TODO(rfindley): use this more widely.
-func (s *snapshot) moduleMode() bool {
- // Since we only really understand the `go` command, if the user has a
- // different GOPACKAGESDRIVER, assume that their configuration is valid.
- if s.view.hasGopackagesDriver {
- return false
- }
-
- switch s.view.effectiveGO111MODULE() {
- case on:
+ if s.view.inGOPATH {
return true
- case off:
- return false
- default:
- return len(s.workspaceModFiles) > 0 || s.view.gowork != ""
}
+
+ return false
}
// workspaceMode describes the way in which the snapshot's workspace should
@@ -652,21 +624,11 @@
return overlays
}
-// TODO(rfindley): investigate whether it would be worthwhile to keep track of
-// overlays when we get them via GetFile.
func (s *snapshot) overlays() []*Overlay {
s.mu.Lock()
defer s.mu.Unlock()
- var overlays []*Overlay
- s.files.Range(func(uri span.URI, fh source.FileHandle) {
- overlay, ok := fh.(*Overlay)
- if !ok {
- return
- }
- overlays = append(overlays, overlay)
- })
- return overlays
+ return s.files.overlays()
}
// Package data kinds, identifying various package data that may be stored in
@@ -763,6 +725,18 @@
}
func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) {
+ if s.view.ViewType() == AdHocView {
+ // As described in golang/go#57209, in ad-hoc workspaces (where we load ./
+ // rather than ./...), preempting the directory load with file loads can
+ // lead to an inconsistent outcome, where certain files are loaded with
+ // command-line-arguments packages and others are loaded only in the ad-hoc
+ // package. Therefore, ensure that the workspace is loaded before doing any
+ // file loads.
+ if err := s.awaitLoaded(ctx); err != nil {
+ return nil, err
+ }
+ }
+
s.mu.Lock()
// Start with the set of package associations derived from the last load.
@@ -962,19 +936,57 @@
patterns[fmt.Sprintf("%s/**/*.{%s}", dirName, extensions)] = struct{}{}
}
- // Some clients do not send notifications for changes to directories that
- // contain Go code (golang/go#42348). To handle this, explicitly watch all
- // of the directories in the workspace. We find them by adding the
- // directories of every file in the snapshot's workspace directories.
- // There may be thousands.
- if pattern := s.getKnownSubdirsPattern(dirs); pattern != "" {
- patterns[pattern] = struct{}{}
+ if s.watchSubdirs() {
+ // Some clients (e.g. VS Code) do not send notifications for changes to
+ // directories that contain Go code (golang/go#42348). To handle this,
+ // explicitly watch all of the directories in the workspace. We find them
+ // by adding the directories of every file in the snapshot's workspace
+ // directories. There may be thousands of patterns, each a single
+ // directory.
+ //
+ // (A previous iteration created a single glob pattern holding a union of
+ // all the directories, but this was found to cause VS Code to get stuck
+ // for several minutes after a buffer was saved twice in a workspace that
+ // had >8000 watched directories.)
+ //
+ // Some clients (notably coc.nvim, which uses watchman for globs) perform
+ // poorly with a large list of individual directories.
+ s.addKnownSubdirs(patterns, dirs)
}
return patterns
}
-func (s *snapshot) getKnownSubdirsPattern(wsDirs []span.URI) string {
+// watchSubdirs reports whether gopls should request separate file watchers for
+// each relevant subdirectory. This is necessary only for clients (namely VS
+// Code) that do not send notifications for individual files in a directory
+// when the entire directory is deleted.
+func (s *snapshot) watchSubdirs() bool {
+ opts := s.view.Options()
+ switch p := opts.SubdirWatchPatterns; p {
+ case source.SubdirWatchPatternsOn:
+ return true
+ case source.SubdirWatchPatternsOff:
+ return false
+ case source.SubdirWatchPatternsAuto:
+ // See the documentation of InternalOptions.SubdirWatchPatterns for an
+ // explanation of why VS Code gets a different default value here.
+ //
+ // Unfortunately, there is no authoritative list of client names, nor any
+ // requirements that client names do not change. We should update the VS
+ // Code extension to set a default value of "subdirWatchPatterns" to "on",
+ // so that this workaround is only temporary.
+ if opts.ClientInfo != nil && opts.ClientInfo.Name == "Visual Studio Code" {
+ return true
+ }
+ return false
+ default:
+ bug.Reportf("invalid subdirWatchPatterns: %q", p)
+ return false
+ }
+}
+
+func (s *snapshot) addKnownSubdirs(patterns map[string]struct{}, wsDirs []span.URI) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -983,23 +995,18 @@
// It may change list of known subdirs and therefore invalidate the cache.
s.applyKnownSubdirsChangesLocked(wsDirs)
- if s.knownSubdirsPatternCache == "" {
- var builder strings.Builder
+ // TODO(adonovan): is it still necessary to memoize the Range
+ // and URI.Filename operations?
+ if s.knownSubdirsCache == nil {
+ s.knownSubdirsCache = make(map[string]struct{})
s.knownSubdirs.Range(func(uri span.URI) {
- if builder.Len() == 0 {
- builder.WriteString("{")
- } else {
- builder.WriteString(",")
- }
- builder.WriteString(uri.Filename())
+ s.knownSubdirsCache[uri.Filename()] = struct{}{}
})
- if builder.Len() > 0 {
- builder.WriteString("}")
- s.knownSubdirsPatternCache = builder.String()
- }
}
- return s.knownSubdirsPatternCache
+ for pattern := range s.knownSubdirsCache {
+ patterns[pattern] = struct{}{}
+ }
}
// collectAllKnownSubdirs collects all of the subdirectories within the
@@ -1013,7 +1020,7 @@
s.knownSubdirs.Destroy()
s.knownSubdirs = newKnownDirsSet()
- s.knownSubdirsPatternCache = ""
+ s.knownSubdirsCache = nil
s.files.Range(func(uri span.URI, fh source.FileHandle) {
s.addKnownSubdirLocked(uri, dirs)
})
@@ -1072,7 +1079,7 @@
}
s.knownSubdirs.Insert(uri)
dir = filepath.Dir(dir)
- s.knownSubdirsPatternCache = ""
+ s.knownSubdirsCache = nil
}
}
@@ -1085,7 +1092,7 @@
}
if info, _ := os.Stat(dir); info == nil {
s.knownSubdirs.Remove(uri)
- s.knownSubdirsPatternCache = ""
+ s.knownSubdirsCache = nil
}
dir = filepath.Dir(dir)
}
@@ -1849,7 +1856,7 @@
fix = `To work with multiple modules simultaneously, please upgrade to Go 1.18 or
later, reinstall gopls, and use a go.work file.`
}
- msg = fmt.Sprintf(`This file is in directory %q, which is not included in your workspace.
+ msg = fmt.Sprintf(`This file is within module %q, which is not included in your workspace.
%s
See the documentation for more information on setting up your workspace:
https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`, modDir, fix)
@@ -2050,7 +2057,7 @@
// changed files. We need to rebuild the workspace module to know the
// true set of known subdirectories, but we don't want to do that in clone.
result.knownSubdirs = s.knownSubdirs.Clone()
- result.knownSubdirsPatternCache = s.knownSubdirsPatternCache
+ result.knownSubdirsCache = s.knownSubdirsCache
for _, c := range changes {
result.unprocessedSubdirChanges = append(result.unprocessedSubdirChanges, c)
}
@@ -2109,10 +2116,36 @@
// Invalidate the previous modTidyHandle if any of the files have been
// saved or if any of the metadata has been invalidated.
if invalidateMetadata || fileWasSaved(originalFH, change.fileHandle) {
- // TODO(maybe): Only delete mod handles for
- // which the withoutURI is relevant.
- // Requires reverse-engineering the go command. (!)
- result.modTidyHandles.Clear()
+ // Only invalidate mod tidy results for the most relevant modfile in the
+ // workspace. This is a potentially lossy optimization for workspaces
+ // with many modules (such as google-cloud-go, which has 145 modules as
+ // of writing).
+ //
+ // While it is theoretically possible that a change in workspace module A
+ // could affect the mod-tidiness of workspace module B (if B transitively
+ // requires A), such changes are probably unlikely and not worth the
+ // penalty of re-running go mod tidy for everything. Note that mod tidy
+ // ignores GOWORK, so the two modules would have to be related by a chain
+ // of replace directives.
+ //
+ // We could improve accuracy by inspecting replace directives, using
+ // overlays in go mod tidy, and/or checking for metadata changes from the
+ // on-disk content.
+ //
+ // Note that we iterate the modTidyHandles map here, rather than e.g.
+ // using nearestModFile, because we don't have access to an accurate
+ // FileSource at this point in the snapshot clone.
+ const onlyInvalidateMostRelevant = true
+ if onlyInvalidateMostRelevant {
+ deleteMostRelevantModFile(result.modTidyHandles, uri)
+ } else {
+ result.modTidyHandles.Clear()
+ }
+
+ // TODO(rfindley): should we apply the above heuristic to mod vuln
+ // or mod handles as well?
+ //
+ // TODO(rfindley): no tests fail if I delete the below line.
result.modWhyHandles.Clear()
result.modVulnHandles.Clear()
}
@@ -2303,6 +2336,31 @@
return result, release
}
+// deleteMostRelevantModFile deletes the mod file most likely to be the mod
+// file for the changed URI, if it exists.
+//
+// Specifically, this is the longest mod file path in a directory containing
+// changed. This might not be accurate if there is another mod file closer to
+// changed that happens not to be present in the map, but that's OK: the goal
+// of this function is to guarantee that IF the nearest mod file is present in
+// the map, it is invalidated.
+func deleteMostRelevantModFile(m *persistent.Map, changed span.URI) {
+ var mostRelevant span.URI
+ changedFile := changed.Filename()
+
+ m.Range(func(key, value interface{}) {
+ modURI := key.(span.URI)
+ if len(modURI) > len(mostRelevant) {
+ if source.InDir(filepath.Dir(modURI.Filename()), changedFile) {
+ mostRelevant = modURI
+ }
+ }
+ })
+ if mostRelevant != "" {
+ m.Delete(mostRelevant)
+ }
+}
+
// invalidatedPackageIDs returns all packages invalidated by a change to uri.
// If we haven't seen this URI before, we guess based on files in the same
// directory. This is of course incorrect in build systems where packages are
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index d999170..1dc13aa 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -130,6 +130,10 @@
// GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on
// their machine.
hasGopackagesDriver bool
+
+ // inGOPATH reports whether the workspace directory is contained in a GOPATH
+ // directory.
+ inGOPATH bool
}
// effectiveGO111MODULE reports the value of GO111MODULE effective in the go
@@ -145,6 +149,79 @@
}
}
+// A ViewType describes how we load package information for a view.
+//
+// This is used for constructing the go/packages.Load query, and for
+// interpreting missing packages, imports, or errors.
+//
+// Each view has a ViewType which is derived from its immutable workspace
+// information -- any environment change that would affect the view type
+// results in a new view.
+type ViewType int
+
+const (
+ // GoPackagesDriverView is a view with a non-empty GOPACKAGESDRIVER
+ // environment variable.
+ GoPackagesDriverView ViewType = iota
+
+ // GOPATHView is a view in GOPATH mode.
+ //
+ // I.e. in GOPATH, with GO111MODULE=off, or GO111MODULE=auto with no
+ // go.mod file.
+ GOPATHView
+
+ // GoModuleView is a view in module mode with a single Go module.
+ GoModuleView
+
+ // GoWorkView is a view in module mode with a go.work file.
+ GoWorkView
+
+ // An AdHocView is a collection of files in a given directory, not in GOPATH
+ // or a module.
+ AdHocView
+)
+
+// ViewType derives the type of the view from its workspace information.
+//
+// TODO(rfindley): this logic is overlapping and slightly inconsistent with
+// validBuildConfiguration. As part of zero-config-gopls (golang/go#57979), fix
+// this inconsistency and consolidate on the ViewType abstraction.
+func (w workspaceInformation) ViewType() ViewType {
+ if w.hasGopackagesDriver {
+ return GoPackagesDriverView
+ }
+ go111module := w.effectiveGO111MODULE()
+ if w.gowork != "" && go111module != off {
+ return GoWorkView
+ }
+ if w.gomod != "" && go111module != off {
+ return GoModuleView
+ }
+ if w.inGOPATH && go111module != on {
+ return GOPATHView
+ }
+ return AdHocView
+}
+
+// moduleMode reports whether the current snapshot uses Go modules.
+//
+// From https://go.dev/ref/mod, module mode is active if either of the
+// following hold:
+// - GO111MODULE=on
+// - GO111MODULE=auto and we are inside a module or have a GOWORK value.
+//
+// Additionally, this method returns false if GOPACKAGESDRIVER is set.
+//
+// TODO(rfindley): use this more widely.
+func (w workspaceInformation) moduleMode() bool {
+ switch w.ViewType() {
+ case GoModuleView, GoWorkView:
+ return true
+ default:
+ return false
+ }
+}
+
// GOWORK returns the effective GOWORK value for this workspace, if
// any, in URI form.
//
@@ -740,6 +817,8 @@
})
}
+ // TODO(rfindley): this should be predicated on the s.view.moduleMode().
+ // There is no point loading ./... if we have an empty go.work.
if len(s.workspaceModFiles) > 0 {
for modURI := range s.workspaceModFiles {
// Verify that the modfile is valid before trying to load it.
@@ -881,7 +960,7 @@
if err != nil {
return info, err
}
- if err := info.goEnv.load(ctx, folder.Filename(), options.EnvSlice(), s.gocmdRunner); err != nil {
+ if err := info.load(ctx, folder.Filename(), options.EnvSlice(), s.gocmdRunner); err != nil {
return info, err
}
// The value of GOPACKAGESDRIVER is not returned through the go command.
@@ -899,6 +978,13 @@
return info, err
}
+ // Check if the workspace is within any GOPATH directory.
+ for _, gp := range filepath.SplitList(info.gopath) {
+ if source.InDir(filepath.Join(gp, "src"), folder.Filename()) {
+ info.inGOPATH = true
+ break
+ }
+ }
return info, nil
}
diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go
index de36da6..28179f5 100644
--- a/gopls/internal/lsp/cache/workspace.go
+++ b/gopls/internal/lsp/cache/workspace.go
@@ -8,6 +8,7 @@
"context"
"errors"
"fmt"
+ "io/fs"
"os"
"path/filepath"
"sort"
@@ -127,7 +128,10 @@
// Limit go.mod search to 1 million files. As a point of reference,
// Kubernetes has 22K files (as of 2020-11-24).
-const fileLimit = 1000000
+//
+// Note: per golang/go#56496, the previous limit of 1M files was too slow, at
+// which point this limit was decreased to 100K.
+const fileLimit = 100_000
// findModules recursively walks the root directory looking for go.mod files,
// returning the set of modules it discovers. If modLimit is non-zero,
@@ -139,7 +143,7 @@
modFiles := make(map[span.URI]struct{})
searched := 0
errDone := errors.New("done")
- err := filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error {
+ err := filepath.WalkDir(root.Filename(), func(path string, info fs.DirEntry, err error) error {
if err != nil {
// Probably a permission error. Keep looking.
return filepath.SkipDir
diff --git a/gopls/internal/lsp/cmd/capabilities_test.go b/gopls/internal/lsp/cmd/capabilities_test.go
index 39b60af..6d4e32f 100644
--- a/gopls/internal/lsp/cmd/capabilities_test.go
+++ b/gopls/internal/lsp/cmd/capabilities_test.go
@@ -41,17 +41,16 @@
defer os.RemoveAll(tmpDir)
app := New("gopls-test", tmpDir, os.Environ(), nil)
- c := newConnection(app, nil)
- ctx := context.Background()
- defer c.terminate(ctx)
params := &protocol.ParamInitialize{}
- params.RootURI = protocol.URIFromPath(c.Client.app.wd)
+ params.RootURI = protocol.URIFromPath(app.wd)
params.Capabilities.Workspace.Configuration = true
// Send an initialize request to the server.
- c.Server = lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), c.Client)
- result, err := c.Server.Initialize(ctx, params)
+ ctx := context.Background()
+ client := newClient(app, nil)
+ server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), client)
+ result, err := server.Initialize(ctx, params)
if err != nil {
t.Fatal(err)
}
@@ -60,10 +59,13 @@
t.Error(err)
}
// Complete initialization of server.
- if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
+ if err := server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
t.Fatal(err)
}
+ c := newConnection(server, client)
+ defer c.terminate(ctx)
+
// Open the file on the server side.
uri := protocol.URIFromPath(tmpFile)
if err := c.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
diff --git a/gopls/internal/lsp/cmd/check.go b/gopls/internal/lsp/cmd/check.go
index f501c44..a529f14 100644
--- a/gopls/internal/lsp/cmd/check.go
+++ b/gopls/internal/lsp/cmd/check.go
@@ -57,8 +57,8 @@
if err := conn.diagnoseFiles(ctx, uris); err != nil {
return err
}
- conn.Client.filesMu.Lock()
- defer conn.Client.filesMu.Unlock()
+ conn.client.filesMu.Lock()
+ defer conn.client.filesMu.Unlock()
for _, file := range checking {
for _, d := range file.diagnostics {
diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go
index 02e135a..0bd636c 100644
--- a/gopls/internal/lsp/cmd/cmd.go
+++ b/gopls/internal/lsp/cmd/cmd.go
@@ -293,10 +293,14 @@
func (app *Application) connect(ctx context.Context, onProgress func(*protocol.ProgressParams)) (*connection, error) {
switch {
case app.Remote == "":
- connection := newConnection(app, onProgress)
- connection.Server = lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), connection.Client)
- ctx = protocol.WithClient(ctx, connection.Client)
- return connection, connection.initialize(ctx, app.options)
+ client := newClient(app, onProgress)
+ server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), client)
+ conn := newConnection(server, client)
+ if err := conn.initialize(protocol.WithClient(ctx, client), app.options); err != nil {
+ return nil, err
+ }
+ return conn, nil
+
case strings.HasPrefix(app.Remote, "internal@"):
internalMu.Lock()
defer internalMu.Unlock()
@@ -331,19 +335,19 @@
}
func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
- connection := newConnection(app, nil)
conn, err := lsprpc.ConnectToRemote(ctx, remote)
if err != nil {
return nil, err
}
stream := jsonrpc2.NewHeaderStream(conn)
cc := jsonrpc2.NewConn(stream)
- connection.Server = protocol.ServerDispatcher(cc)
- ctx = protocol.WithClient(ctx, connection.Client)
+ server := protocol.ServerDispatcher(cc)
+ client := newClient(app, nil)
+ connection := newConnection(server, client)
+ ctx = protocol.WithClient(ctx, connection.client)
cc.Go(ctx,
protocol.Handlers(
- protocol.ClientHandler(connection.Client,
- jsonrpc2.MethodNotFound)))
+ protocol.ClientHandler(client, jsonrpc2.MethodNotFound)))
return connection, connection.initialize(ctx, app.options)
}
@@ -355,7 +359,7 @@
func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
params := &protocol.ParamInitialize{}
- params.RootURI = protocol.URIFromPath(c.Client.app.wd)
+ params.RootURI = protocol.URIFromPath(c.client.app.wd)
params.Capabilities.Workspace.Configuration = true
// Make sure to respect configured options when sending initialize request.
@@ -377,7 +381,7 @@
// If the subcommand has registered a progress handler, report the progress
// capability.
- if c.Client.onProgress != nil {
+ if c.client.onProgress != nil {
params.Capabilities.Window.WorkDoneProgress = true
}
@@ -395,11 +399,10 @@
type connection struct {
protocol.Server
- Client *cmdClient
+ client *cmdClient
}
type cmdClient struct {
- protocol.Server
app *Application
onProgress func(*protocol.ProgressParams)
@@ -417,13 +420,18 @@
diagnostics []protocol.Diagnostic
}
-func newConnection(app *Application, onProgress func(*protocol.ProgressParams)) *connection {
+func newClient(app *Application, onProgress func(*protocol.ProgressParams)) *cmdClient {
+ return &cmdClient{
+ app: app,
+ onProgress: onProgress,
+ files: make(map[span.URI]*cmdFile),
+ }
+}
+
+func newConnection(server protocol.Server, client *cmdClient) *connection {
return &connection{
- Client: &cmdClient{
- app: app,
- onProgress: onProgress,
- files: make(map[span.URI]*cmdFile),
- },
+ Server: server,
+ client: client,
}
}
@@ -518,11 +526,6 @@
}
func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
- var debug = os.Getenv(DebugSuggestedFixEnvVar) == "true"
- if debug {
- log.Printf("PublishDiagnostics URI=%v Diagnostics=%v", p.URI, p.Diagnostics)
- }
-
if p.URI == "gopls://diagnostics-done" {
close(c.diagnosticsDone)
}
@@ -616,7 +619,7 @@
// - map a (URI, protocol.Range) to a MappedRange;
// - parse a command-line argument to a MappedRange.
func (c *connection) openFile(ctx context.Context, uri span.URI) (*cmdFile, error) {
- file := c.Client.openFile(ctx, uri)
+ file := c.client.openFile(ctx, uri)
if file.err != nil {
return nil, file.err
}
@@ -651,22 +654,22 @@
for _, file := range files {
untypedFiles = append(untypedFiles, string(file))
}
- c.Client.diagnosticsMu.Lock()
- defer c.Client.diagnosticsMu.Unlock()
+ c.client.diagnosticsMu.Lock()
+ defer c.client.diagnosticsMu.Unlock()
- c.Client.diagnosticsDone = make(chan struct{})
+ c.client.diagnosticsDone = make(chan struct{})
_, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles})
if err != nil {
- close(c.Client.diagnosticsDone)
+ close(c.client.diagnosticsDone)
return err
}
- <-c.Client.diagnosticsDone
+ <-c.client.diagnosticsDone
return nil
}
func (c *connection) terminate(ctx context.Context) {
- if strings.HasPrefix(c.Client.app.Remote, "internal@") {
+ if strings.HasPrefix(c.client.app.Remote, "internal@") {
// internal connections need to be left alive for the next test
return
}
diff --git a/gopls/internal/lsp/cmd/serve.go b/gopls/internal/lsp/cmd/serve.go
index df42e79..03cc187 100644
--- a/gopls/internal/lsp/cmd/serve.go
+++ b/gopls/internal/lsp/cmd/serve.go
@@ -90,7 +90,6 @@
}
defer closeLog()
di.ServerAddress = s.Address
- di.MonitorMemory(ctx)
di.Serve(ctx, s.Debug)
}
var ss jsonrpc2.StreamServer
diff --git a/gopls/internal/lsp/cmd/stats.go b/gopls/internal/lsp/cmd/stats.go
index 1b9df2f..a681c5c 100644
--- a/gopls/internal/lsp/cmd/stats.go
+++ b/gopls/internal/lsp/cmd/stats.go
@@ -9,9 +9,11 @@
"encoding/json"
"flag"
"fmt"
+ "go/token"
"io/fs"
"os"
"path/filepath"
+ "reflect"
"runtime"
"strings"
"sync"
@@ -24,10 +26,13 @@
"golang.org/x/tools/gopls/internal/lsp/filecache"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/internal/event"
)
type stats struct {
app *Application
+
+ Anon bool `flag:"anon" help:"hide any fields that may contain user names, file names, or source code"`
}
func (s *stats) Name() string { return "stats" }
@@ -41,14 +46,17 @@
workspace information relevant to performance. As a side effect, this command
populates the gopls file cache for the current workspace.
+By default, this command may include output that refers to the location or
+content of user code. When the -anon flag is set, fields that may refer to user
+code are hidden.
+
Example:
- $ gopls stats
+ $ gopls stats -anon
`)
printFlagDefaults(f)
}
func (s *stats) Run(ctx context.Context, args ...string) error {
-
// This undocumented environment variable allows
// the cmd integration test to trigger a call to bug.Report.
if msg := os.Getenv("TEST_GOPLS_BUG"); msg != "" {
@@ -65,9 +73,14 @@
return fmt.Errorf("the stats subcommand does not work with -remote")
}
+ if !s.app.Verbose {
+ event.SetExporter(nil) // don't log errors to stderr
+ }
+
stats := GoplsStats{
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
+ GOPLSCACHE: os.Getenv("GOPLSCACHE"),
GoVersion: runtime.Version(),
GoplsVersion: debug.Version,
}
@@ -138,10 +151,10 @@
// Gather bug reports produced by any process using
// this executable and persisted in the cache.
- stats.BugReports = []string{} // non-nil for JSON
do("Gathering bug reports", func() error {
- for _, report := range filecache.BugReports() {
- stats.BugReports = append(stats.BugReports, string(report))
+ stats.CacheDir, stats.BugReports = filecache.BugReports()
+ if stats.BugReports == nil {
+ stats.BugReports = []goplsbug.Bug{} // non-nil for JSON
}
return nil
})
@@ -183,24 +196,51 @@
return err
}
- data, err := json.MarshalIndent(stats, "", " ")
+ // Filter JSON output to fields that are consistent with s.Anon.
+ okFields := make(map[string]interface{})
+ {
+ v := reflect.ValueOf(stats)
+ t := v.Type()
+ for i := 0; i < t.NumField(); i++ {
+ f := t.Field(i)
+ if !token.IsExported(f.Name) {
+ continue
+ }
+ if s.Anon && f.Tag.Get("anon") != "ok" {
+ // Fields that can be served with -anon must be explicitly marked as OK.
+ continue
+ }
+ vf := v.FieldByName(f.Name)
+ okFields[f.Name] = vf.Interface()
+ }
+ }
+ data, err := json.MarshalIndent(okFields, "", " ")
if err != nil {
return err
}
+
os.Stdout.Write(data)
fmt.Println()
return nil
}
+// GoplsStats holds information extracted from a gopls session in the current
+// workspace.
+//
+// Fields that should be printed with the -anon flag should be explicitly
+// marked as `anon:"ok"`. Only fields that cannot refer to user files or code
+// should be marked as such.
type GoplsStats struct {
- GOOS, GOARCH string
- GoVersion string
- GoplsVersion string
- InitialWorkspaceLoadDuration string // in time.Duration string form
- BugReports []string
- MemStats command.MemStatsResult
- WorkspaceStats command.WorkspaceStatsResult
- DirStats dirStats
+ GOOS, GOARCH string `anon:"ok"`
+ GOPLSCACHE string
+ GoVersion string `anon:"ok"`
+ GoplsVersion string `anon:"ok"`
+ InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form
+ CacheDir string
+ BugReports []goplsbug.Bug
+ MemStats command.MemStatsResult `anon:"ok"`
+ WorkspaceStats command.WorkspaceStatsResult `anon:"ok"`
+ DirStats dirStats `anon:"ok"`
}
type dirStats struct {
diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go
index 1128688..169d6d1 100644
--- a/gopls/internal/lsp/cmd/suggested_fix.go
+++ b/gopls/internal/lsp/cmd/suggested_fix.go
@@ -9,7 +9,6 @@
"flag"
"fmt"
"io/ioutil"
- "log"
"os"
"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -42,16 +41,11 @@
printFlagDefaults(f)
}
-const DebugSuggestedFixEnvVar = "_DEBUG_SUGGESTED_FIX"
-
// Run performs diagnostic checks on the file specified and either;
// - if -w is specified, updates the file in place;
// - if -d is specified, prints out unified diffs of the changes; or
// - otherwise, prints the new versions to stdout.
func (s *suggestedFix) Run(ctx context.Context, args ...string) error {
- // For debugging golang/go#59475, enable some additional output.
- var debug = os.Getenv(DebugSuggestedFixEnvVar) == "true"
-
if len(args) < 1 {
return tool.CommandLineErrorf("fix expects at least 1 argument")
}
@@ -77,12 +71,9 @@
return err
}
diagnostics := []protocol.Diagnostic{} // LSP wants non-nil slice
- conn.Client.filesMu.Lock()
+ conn.client.filesMu.Lock()
diagnostics = append(diagnostics, file.diagnostics...)
- conn.Client.filesMu.Unlock()
- if debug {
- log.Printf("file diagnostics: %#v", diagnostics)
- }
+ conn.client.filesMu.Unlock()
// Request code actions
codeActionKinds := []protocol.CodeActionKind{protocol.QuickFix}
@@ -106,9 +97,6 @@
if err != nil {
return fmt.Errorf("%v: %v", from, err)
}
- if debug {
- log.Printf("code actions: %#v", actions)
- }
// Gather edits from matching code actions.
var edits []protocol.TextEdit
diff --git a/gopls/internal/lsp/cmd/test/integration_test.go b/gopls/internal/lsp/cmd/test/integration_test.go
index c95790c..52ecb23 100644
--- a/gopls/internal/lsp/cmd/test/integration_test.go
+++ b/gopls/internal/lsp/cmd/test/integration_test.go
@@ -756,6 +756,18 @@
}
}
+ // Check that -anon suppresses fields containing user information.
+ {
+ res2 := gopls(t, tree, "stats", "-anon")
+ res2.checkExit(true)
+ var stats2 cmd.GoplsStats
+ if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil {
+ t.Fatalf("failed to unmarshal JSON output of stats command: %v", err)
+ }
+ if got := len(stats2.BugReports); got > 0 {
+ t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports)
+ }
+ }
}
// TestFix tests the 'fix' subcommand (../suggested_fix.go).
@@ -890,10 +902,7 @@
}
goplsCmd := exec.Command(os.Args[0], args...)
- goplsCmd.Env = append(os.Environ(),
- "ENTRYPOINT=goplsMain",
- fmt.Sprintf("%s=true", cmd.DebugSuggestedFixEnvVar),
- )
+ goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain")
goplsCmd.Env = append(goplsCmd.Env, env...)
goplsCmd.Dir = dir
goplsCmd.Stdout = new(bytes.Buffer)
diff --git a/gopls/internal/lsp/cmd/usage/stats.hlp b/gopls/internal/lsp/cmd/usage/stats.hlp
index 7694e29..71cce07 100644
--- a/gopls/internal/lsp/cmd/usage/stats.hlp
+++ b/gopls/internal/lsp/cmd/usage/stats.hlp
@@ -7,5 +7,11 @@
workspace information relevant to performance. As a side effect, this command
populates the gopls file cache for the current workspace.
+By default, this command may include output that refers to the location or
+content of user code. When the -anon flag is set, fields that may refer to user
+code are hidden.
+
Example:
- $ gopls stats
+ $ gopls stats -anon
+ -anon
+ hide any fields that may contain user names, file names, or source code
diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go
index 8658ba55..8e817b8 100644
--- a/gopls/internal/lsp/code_action.go
+++ b/gopls/internal/lsp/code_action.go
@@ -473,6 +473,17 @@
func codeActionsMatchingDiagnostics(ctx context.Context, snapshot source.Snapshot, pdiags []protocol.Diagnostic, sdiags []*source.Diagnostic) ([]protocol.CodeAction, error) {
var actions []protocol.CodeAction
+ var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field
+ for _, pd := range pdiags {
+ bundled := source.BundledQuickFixes(pd)
+ if len(bundled) > 0 {
+ actions = append(actions, bundled...)
+ } else {
+ // No bundled actions: keep searching for a match.
+ unbundled = append(unbundled, pd)
+ }
+ }
+
for _, sd := range sdiags {
var diag *protocol.Diagnostic
for _, pd := range pdiags {
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index 7236087..7bbadc1 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -360,10 +360,9 @@
progress: "Removing dependency",
forURI: args.URI,
}, func(ctx context.Context, deps commandDeps) error {
- // 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
+ // See the documentation for OnlyDiagnostic.
+ //
+ // TODO(rfindley): In Go 1.17+, we will be able to use the go command
// without checking if the module is tidy.
if args.OnlyDiagnostic {
return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go
index 1342e84..ababac6 100644
--- a/gopls/internal/lsp/command/interface.go
+++ b/gopls/internal/lsp/command/interface.go
@@ -236,7 +236,10 @@
// The go.mod file URI.
URI protocol.DocumentURI
// The module path to remove.
- ModulePath string
+ ModulePath string
+ // 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.
OnlyDiagnostic bool
}
diff --git a/gopls/internal/lsp/debug/info.go b/gopls/internal/lsp/debug/info.go
index 34fe216..fec4562 100644
--- a/gopls/internal/lsp/debug/info.go
+++ b/gopls/internal/lsp/debug/info.go
@@ -10,6 +10,7 @@
"encoding/json"
"fmt"
"io"
+ "os"
"reflect"
"runtime"
"runtime/debug"
@@ -67,6 +68,7 @@
section(w, HTML, "Server Instance", func() {
fmt.Fprintf(w, "Start time: %v\n", i.StartTime)
fmt.Fprintf(w, "LogFile: %s\n", i.Logfile)
+ fmt.Fprintf(w, "pid: %d\n", os.Getpid())
fmt.Fprintf(w, "Working directory: %s\n", i.Workdir)
fmt.Fprintf(w, "Address: %s\n", i.ServerAddress)
fmt.Fprintf(w, "Debug address: %s\n", i.DebugAddress())
diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go
index 3c17dad..f36a238 100644
--- a/gopls/internal/lsp/debug/serve.go
+++ b/gopls/internal/lsp/debug/serve.go
@@ -5,7 +5,6 @@
package debug
import (
- "archive/zip"
"bytes"
"context"
"errors"
@@ -20,7 +19,6 @@
"path"
"path/filepath"
"runtime"
- rpprof "runtime/pprof"
"strconv"
"strings"
"sync"
@@ -494,65 +492,6 @@
return i.listenedDebugAddress
}
-// MonitorMemory starts recording memory statistics each second.
-func (i *Instance) MonitorMemory(ctx context.Context) {
- tick := time.NewTicker(time.Second)
- nextThresholdGiB := uint64(1)
- go func() {
- for {
- <-tick.C
- var mem runtime.MemStats
- runtime.ReadMemStats(&mem)
- if mem.HeapAlloc < nextThresholdGiB*1<<30 {
- continue
- }
- if err := i.writeMemoryDebug(nextThresholdGiB, true); err != nil {
- event.Error(ctx, "writing memory debug info", err)
- }
- if err := i.writeMemoryDebug(nextThresholdGiB, false); err != nil {
- event.Error(ctx, "writing memory debug info", err)
- }
- event.Log(ctx, fmt.Sprintf("Wrote memory usage debug info to %v", os.TempDir()))
- nextThresholdGiB++
- }
- }()
-}
-
-func (i *Instance) writeMemoryDebug(threshold uint64, withNames bool) error {
- suffix := "withnames"
- if !withNames {
- suffix = "nonames"
- }
-
- filename := fmt.Sprintf("gopls.%d-%dGiB-%s.zip", os.Getpid(), threshold, suffix)
- zipf, err := os.OpenFile(filepath.Join(os.TempDir(), filename), os.O_CREATE|os.O_RDWR, 0644)
- if err != nil {
- return err
- }
- zipw := zip.NewWriter(zipf)
-
- f, err := zipw.Create("heap.pb.gz")
- if err != nil {
- return err
- }
- if err := rpprof.Lookup("heap").WriteTo(f, 0); err != nil {
- return err
- }
-
- f, err = zipw.Create("goroutines.txt")
- if err != nil {
- return err
- }
- if err := rpprof.Lookup("goroutine").WriteTo(f, 1); err != nil {
- return err
- }
-
- if err := zipw.Close(); err != nil {
- return err
- }
- return zipf.Close()
-}
-
func makeGlobalExporter(stderr io.Writer) event.Exporter {
p := export.Printer{}
var pMu sync.Mutex
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index 90c2232..88008d3 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -145,6 +145,9 @@
fmt.Fprintf(h, "range: %s\n", d.Range)
fmt.Fprintf(h, "severity: %s\n", d.Severity)
fmt.Fprintf(h, "source: %s\n", d.Source)
+ if d.BundledFixes != nil {
+ fmt.Fprintf(h, "fixes: %s\n", *d.BundledFixes)
+ }
}
return fmt.Sprintf("%x", h.Sum(nil))
}
@@ -771,6 +774,7 @@
Source: string(diag.Source),
Tags: emptySliceDiagnosticTag(diag.Tags),
RelatedInformation: diag.Related,
+ Data: diag.BundledFixes,
}
if diag.Code != "" {
pdiag.Code = diag.Code
diff --git a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go
index b619ef5..555428e 100644
--- a/gopls/internal/lsp/fake/client.go
+++ b/gopls/internal/lsp/fake/client.go
@@ -94,9 +94,8 @@
results := make([]interface{}, len(p.Items))
for i, item := range p.Items {
if item.Section == "gopls" {
- c.editor.mu.Lock()
- results[i] = c.editor.settingsLocked()
- c.editor.mu.Unlock()
+ config := c.editor.Config()
+ results[i] = makeSettings(c.editor.sandbox, config)
}
}
return results, nil
diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go
index ae9338d..45def8f 100644
--- a/gopls/internal/lsp/fake/editor.go
+++ b/gopls/internal/lsp/fake/editor.go
@@ -37,7 +37,6 @@
serverConn jsonrpc2.Conn
client *Client
sandbox *Sandbox
- defaultEnv map[string]string
// TODO(adonovan): buffers should be keyed by protocol.DocumentURI.
mu sync.Mutex
@@ -75,8 +74,14 @@
// source.UserOptions, but we use a separate type here so that we expose only
// that configuration which we support.
//
-// The zero value for EditorConfig should correspond to its defaults.
+// The zero value for EditorConfig is the default configuration.
type EditorConfig struct {
+ // ClientName sets the clientInfo.name for the LSP session (in the initialize request).
+ //
+ // Since this can only be set during initialization, changing this field via
+ // Editor.ChangeConfiguration has no effect.
+ ClientName string
+
// Env holds environment variables to apply on top of the default editor
// environment. When applying these variables, the special string
// $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working
@@ -109,10 +114,9 @@
// NewEditor creates a new Editor.
func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
return &Editor{
- buffers: make(map[string]buffer),
- sandbox: sandbox,
- defaultEnv: sandbox.GoEnv(),
- config: config,
+ buffers: make(map[string]buffer),
+ sandbox: sandbox,
+ config: config,
}
}
@@ -198,19 +202,17 @@
return e.client
}
-// settingsLocked builds the settings map for use in LSP settings RPCs.
-//
-// e.mu must be held while calling this function.
-func (e *Editor) settingsLocked() map[string]interface{} {
+// makeSettings builds the settings map for use in LSP settings RPCs.
+func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} {
env := make(map[string]string)
- for k, v := range e.defaultEnv {
+ for k, v := range sandbox.GoEnv() {
env[k] = v
}
- for k, v := range e.config.Env {
+ for k, v := range config.Env {
env[k] = v
}
for k, v := range env {
- v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename())
+ v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", sandbox.Workdir.RootURI().SpanURI().Filename())
env[k] = v
}
@@ -226,7 +228,7 @@
"completionBudget": "10s",
}
- for k, v := range e.config.Settings {
+ for k, v := range config.Settings {
if k == "env" {
panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead")
}
@@ -237,20 +239,22 @@
}
func (e *Editor) initialize(ctx context.Context) error {
+ config := e.Config()
+
params := &protocol.ParamInitialize{}
- params.ClientInfo = &protocol.Msg_XInitializeParams_clientInfo{}
- params.ClientInfo.Name = "fakeclient"
- params.ClientInfo.Version = "v1.0.0"
- e.mu.Lock()
- params.WorkspaceFolders = e.makeWorkspaceFoldersLocked()
- params.InitializationOptions = e.settingsLocked()
- e.mu.Unlock()
- params.Capabilities.Workspace.Configuration = true
- params.Capabilities.Window.WorkDoneProgress = true
+ if e.config.ClientName != "" {
+ params.ClientInfo = &protocol.Msg_XInitializeParams_clientInfo{}
+ params.ClientInfo.Name = e.config.ClientName
+ params.ClientInfo.Version = "v1.0.0"
+ }
+ params.InitializationOptions = makeSettings(e.sandbox, config)
+ params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
+ params.Capabilities.Workspace.Configuration = true // support workspace/configuration
+ params.Capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress
- // TODO: set client capabilities
+ // TODO(rfindley): set client capabilities (note from the future: why?)
+
params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
-
params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
params.Capabilities.TextDocument.SemanticTokens.Requests.Full.Value = true
// copied from lsp/semantic.go to avoid import cycle in tests
@@ -269,11 +273,12 @@
// but really we should test both ways for older editors.
params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true
- // This is a bit of a hack, since the fake editor doesn't actually support
- // watching changed files that match a specific glob pattern. However, the
- // editor does send didChangeWatchedFiles notifications, so set this to
- // true.
+ // Glob pattern watching is enabled.
params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
+
+ // "rename" operations are used for package renaming.
+ //
+ // TODO(rfindley): add support for other resource operations (create, delete, ...)
params.Capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{
ResourceOperations: []protocol.ResourceOperationKind{
"rename",
@@ -300,18 +305,15 @@
return nil
}
-// makeWorkspaceFoldersLocked creates a slice of workspace folders to use for
+// makeWorkspaceFolders creates a slice of workspace folders to use for
// this editing session, based on the editor configuration.
-//
-// e.mu must be held while calling this function.
-func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolder) {
- paths := e.config.WorkspaceFolders
+func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) {
if len(paths) == 0 {
- paths = append(paths, string(e.sandbox.Workdir.RelativeTo))
+ paths = []string{string(sandbox.Workdir.RelativeTo)}
}
for _, path := range paths {
- uri := string(e.sandbox.Workdir.URI(path))
+ uri := string(sandbox.Workdir.URI(path))
folders = append(folders, protocol.WorkspaceFolder{
URI: uri,
Name: filepath.Base(uri),
@@ -1329,14 +1331,18 @@
return e.config
}
+func (e *Editor) SetConfig(cfg EditorConfig) {
+ e.mu.Lock()
+ e.config = cfg
+ e.mu.Unlock()
+}
+
// ChangeConfiguration sets the new editor configuration, and if applicable
// sends a didChangeConfiguration notification.
//
// An error is returned if the change notification failed to send.
func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error {
- e.mu.Lock()
- e.config = newConfig
- e.mu.Unlock() // don't hold e.mu during server calls
+ e.SetConfig(newConfig)
if e.Server != nil {
var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field
if err := e.Server.DidChangeConfiguration(ctx, ¶ms); err != nil {
@@ -1351,12 +1357,13 @@
//
// The given folders must all be unique.
func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error {
+ config := e.Config()
+
// capture existing folders so that we can compute the change.
- e.mu.Lock()
- oldFolders := e.makeWorkspaceFoldersLocked()
- e.config.WorkspaceFolders = folders
- newFolders := e.makeWorkspaceFoldersLocked()
- e.mu.Unlock()
+ oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
+ newFolders := makeWorkspaceFolders(e.sandbox, folders)
+ config.WorkspaceFolders = folders
+ e.SetConfig(config)
if e.Server == nil {
return nil
diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go
index c4e2ce4..df84693 100644
--- a/gopls/internal/lsp/filecache/filecache.go
+++ b/gopls/internal/lsp/filecache/filecache.go
@@ -23,25 +23,23 @@
import (
"bytes"
"crypto/sha256"
- "encoding/binary"
"encoding/hex"
+ "encoding/json"
"errors"
"fmt"
- "hash/crc32"
"io"
"io/fs"
"log"
"os"
"path/filepath"
- "runtime"
"sort"
+ "strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/tools/gopls/internal/bug"
"golang.org/x/tools/gopls/internal/lsp/lru"
- "golang.org/x/tools/internal/lockedfile"
)
// Start causes the filecache to initialize and start garbage gollection.
@@ -77,60 +75,60 @@
iolimit <- struct{}{} // acquire a token
defer func() { <-iolimit }() // release a token
- name, err := filename(kind, key)
+ // Read the index file, which provides the name of the CAS file.
+ indexName, err := filename(kind, key)
if err != nil {
return nil, err
}
- data, err := lockedfile.Read(name)
+ indexData, err := os.ReadFile(indexName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotFound
}
return nil, err
}
-
- // Verify that the Write was complete
- // by checking the recorded length.
- if len(data) < 8+4 {
- return nil, ErrNotFound // cache entry is incomplete
- }
- length, value, checksum := data[:8], data[8:len(data)-4], data[len(data)-4:]
- if binary.LittleEndian.Uint64(length) != uint64(len(value)) {
- return nil, ErrNotFound // cache entry is incomplete (or too long!)
+ var valueHash [32]byte
+ if copy(valueHash[:], indexData) != len(valueHash) {
+ return nil, ErrNotFound // index entry has wrong length
}
- // Check for corruption and print the entire file content; see
- // issue #59289. TODO(adonovan): stop printing the entire file
- // once we've seen enough reports to understand the pattern.
- if binary.LittleEndian.Uint32(checksum) != crc32.ChecksumIEEE(value) {
- // Darwin has repeatedly displayed a problem (#59895)
- // whereby the checksum portion (and only it) is zero,
- // which suggests a bug in its file system . Don't
- // panic, but keep an eye on other failures for now.
- errorf := bug.Errorf
- if binary.LittleEndian.Uint32(checksum) == 0 && runtime.GOOS == "darwin" {
- errorf = fmt.Errorf
- }
-
- return nil, errorf("internal error in filecache.Get(%q, %x): invalid checksum at end of %d-byte file %s:\n%q",
- kind, key, len(data), name, data)
+ // Read the CAS file and check its contents match.
+ //
+ // This ensures integrity in all cases (corrupt or truncated
+ // file, short read, I/O error, wrong length, etc) except an
+ // engineered hash collision, which is infeasible.
+ casName, err := filename(casKind, valueHash)
+ if err != nil {
+ return nil, err
+ }
+ value, _ := os.ReadFile(casName) // ignore error
+ if sha256.Sum256(value) != valueHash {
+ return nil, ErrNotFound // CAS file is missing or has wrong contents
}
- // Update file time for use by LRU eviction.
- // (This turns every read into a write operation.
- // If this is a performance problem, we should
- // touch the files aynchronously.)
+ // Update file times used by LRU eviction.
+ //
+ // Because this turns a read into a write operation,
+ // we follow the approach used in the go command's
+ // cache and update the access time only if the
+ // existing timestamp is older than one hour.
//
// (Traditionally the access time would be updated
// automatically, but for efficiency most POSIX systems have
// for many years set the noatime mount option to avoid every
// open or read operation entailing a metadata write.)
now := time.Now()
- if err := os.Chtimes(name, now, now); err != nil {
- return nil, fmt.Errorf("failed to update access time: %w", err)
+ touch := func(filename string) {
+ st, err := os.Stat(filename)
+ if err == nil && now.Sub(st.ModTime()) > time.Hour {
+ os.Chtimes(filename, now, now) // ignore error
+ }
}
+ touch(indexName)
+ touch(casName)
memCache.Set(memKey{kind, key}, value, len(value))
+
return value, nil
}
@@ -145,56 +143,81 @@
iolimit <- struct{}{} // acquire a token
defer func() { <-iolimit }() // release a token
- name, err := filename(kind, key)
+ // First, add the value to the content-
+ // addressable store (CAS), if not present.
+ hash := sha256.Sum256(value)
+ casName, err := filename(casKind, hash)
if err != nil {
return err
}
- if err := os.MkdirAll(filepath.Dir(name), 0700); err != nil {
- return err
+ // Does CAS file exist and have correct (complete) content?
+ // TODO(adonovan): opt: use mmap for this check.
+ if prev, _ := os.ReadFile(casName); !bytes.Equal(prev, value) {
+ if err := os.MkdirAll(filepath.Dir(casName), 0700); err != nil {
+ return err
+ }
+ // Avoiding O_TRUNC here is merely an optimization to avoid
+ // cache misses when two threads race to write the same file.
+ if err := writeFileNoTrunc(casName, value, 0600); err != nil {
+ os.Remove(casName) // ignore error
+ return err // e.g. disk full
+ }
}
- // In the unlikely event of a short write (e.g. ENOSPC)
- // followed by process termination (e.g. a power cut), we
- // don't want a reader to see a short file, so we record
- // the expected length first and verify it in Get.
- var length [8]byte
- binary.LittleEndian.PutUint64(length[:], uint64(len(value)))
+ // Now write an index entry that refers to the CAS file.
+ indexName, err := filename(kind, key)
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(filepath.Dir(indexName), 0700); err != nil {
+ return err
+ }
+ if err := writeFileNoTrunc(indexName, hash[:], 0600); err != nil {
+ os.Remove(indexName) // ignore error
+ return err // e.g. disk full
+ }
- // Occasional file corruption (presence of zero bytes in JSON
- // files) has been reported on macOS (see issue #59289),
- // assumed due to a nonatomicity problem in the file system.
- // Ideally the macOS kernel would be fixed, or lockedfile
- // would implement a workaround (since its job is to provide
- // reliable the mutual exclusion primitive that allows
- // cooperating gopls processes to implement transactional
- // file replacement), but for now we add an extra integrity
- // check: a 32-bit checksum at the end.
- var checksum [4]byte
- binary.LittleEndian.PutUint32(checksum[:], crc32.ChecksumIEEE(value))
-
- // Windows doesn't support atomic rename--we tried MoveFile,
- // MoveFileEx, ReplaceFileEx, and SetFileInformationByHandle
- // of RenameFileInfo, all to no avail--so instead we use
- // advisory file locking, which is only about 2x slower even
- // on POSIX platforms with atomic rename.
- return lockedfile.Write(name, io.MultiReader(
- bytes.NewReader(length[:]),
- bytes.NewReader(value),
- bytes.NewReader(checksum[:])),
- 0600)
+ return nil
}
+// writeFileNoTrunc is like os.WriteFile but doesn't truncate until
+// after the write, so that racing writes of the same data are idempotent.
+func writeFileNoTrunc(filename string, data []byte, perm os.FileMode) error {
+ f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, perm)
+ if err != nil {
+ return err
+ }
+ _, err = f.Write(data)
+ if err == nil {
+ err = f.Truncate(int64(len(data)))
+ }
+ if closeErr := f.Close(); err == nil {
+ err = closeErr
+ }
+ return err
+}
+
+// reserved kind strings
+const (
+ casKind = "cas" // content-addressable store files
+ bugKind = "bug" // gopls bug reports
+)
+
var iolimit = make(chan struct{}, 128) // counting semaphore to limit I/O concurrency in Set.
var budget int64 = 1e9 // 1GB
-// SetBudget sets a soft limit on disk usage of the cache (in bytes)
-// and returns the previous value. Supplying a negative value queries
-// the current value without changing it.
+// SetBudget sets a soft limit on disk usage of files in the cache (in
+// bytes) and returns the previous value. Supplying a negative value
+// queries the current value without changing it.
//
// If two gopls processes have different budgets, the one with the
// lower budget will collect garbage more actively, but both will
// observe the effect.
+//
+// Even in the steady state, the storage usage reported by the 'du'
+// command may exceed the budget by as much as 50-70% due to the
+// overheads of directories and the effects of block quantization.
func SetBudget(new int64) (old int64) {
if new < 0 {
return atomic.LoadInt64(&budget)
@@ -204,22 +227,62 @@
// --- implementation ----
-// filename returns the cache entry of the specified kind and key.
+// filename returns the name of the cache file of the specified kind and key.
//
-// A typical cache entry is a file name such as:
+// A typical cache file has a name such as:
//
-// $HOME/Library/Caches / gopls / VVVVVVVV / kind / KK / KKKK...KKKK
+// $HOME/Library/Caches / gopls / VVVVVVVV / KK / KKKK...KKKK - kind
//
// The portions separated by spaces are as follows:
// - The user's preferred cache directory; the default value varies by OS.
// - The constant "gopls".
// - The "version", 32 bits of the digest of the gopls executable.
-// - The kind or purpose of this cache subtree (e.g. "analysis").
// - The first 8 bits of the key, to avoid huge directories.
// - The full 256 bits of the key.
+// - The kind or purpose of this cache file (e.g. "analysis").
//
-// Once a file is written its contents are never modified, though it
-// may be atomically replaced or removed.
+// The kind establishes a namespace for the keys. It is represented as
+// a suffix, not a segment, as this significantly reduces the number
+// of directories created, and thus the storage overhead.
+//
+// Previous iterations of the design aimed for the invariant that once
+// a file is written, its contents are never modified, though it may
+// be atomically replaced or removed. However, not all platforms have
+// an atomic rename operation (our first approach), and file locking
+// (our second) is a notoriously fickle mechanism.
+//
+// The current design instead exploits a trick from the cache
+// implementation used by the go command: writes of small files are in
+// practice atomic (all or nothing) on all platforms.
+// (See GOROOT/src/cmd/go/internal/cache/cache.go.)
+//
+// Russ Cox notes: "all file systems use an rwlock around every file
+// system block, including data blocks, so any writes or reads within
+// the same block are going to be handled atomically by the FS
+// implementation without any need to request file locking explicitly.
+// And since the files are so small, there's only one block. (A block
+// is at minimum 512 bytes, usually much more.)" And: "all modern file
+// systems protect against [partial writes due to power loss] with
+// journals."
+//
+// We use a two-level scheme consisting of an index and a
+// content-addressable store (CAS). A single cache entry consists of
+// two files. The value of a cache entry is written into the file at
+// filename("cas", sha256(value)). Since the value may be arbitrarily
+// large, this write is not atomic. That means we must check the
+// integrity of the contents read back from the CAS to make sure they
+// hash to the expected key. If the CAS file is incomplete or
+// inconsistent, we proceed as if it were missing.
+//
+// Once the CAS file has been written, we write a small fixed-size
+// index file at filename(kind, key), using the values supplied by the
+// caller. The index file contains the hash that identifies the value
+// file in the CAS. (We could add extra metadata to this file, up to
+// 512B, the minimum size of a disk block, if later desired, so long
+// as the total size remains fixed.) Because the index file is small,
+// concurrent writes to it are atomic in practice, even though this is
+// not guaranteed by any OS. The fixed size ensures that readers can't
+// see a palimpsest when a short new file overwrites a longer old one.
//
// New versions of gopls are free to reorganize the contents of the
// version directory as needs evolve. But all versions of gopls must
@@ -230,12 +293,13 @@
// after older ones: in the development cycle especially, new
// new versions may be created frequently.
func filename(kind string, key [32]byte) (string, error) {
- hex := fmt.Sprintf("%x", key)
+ base := fmt.Sprintf("%x-%s", key, kind)
dir, err := getCacheDir()
if err != nil {
return "", err
}
- return filepath.Join(dir, kind, hex[:2], hex), nil
+ // Keep the BugReports function consistent with this one.
+ return filepath.Join(dir, base[:2], base), nil
}
// getCacheDir returns the persistent cache directory of all processes
@@ -462,8 +526,6 @@
}
}
-const bugKind = "bug" // reserved kind for gopls bug reports
-
func init() {
// Register a handler to durably record this process's first
// assertion failure in the cache so that we can ask users to
@@ -472,37 +534,51 @@
// Wait for cache init (bugs in tests happen early).
_, _ = getCacheDir()
- value := []byte(fmt.Sprintf("%s: %+v", time.Now().Format(time.RFC3339), bug))
- key := sha256.Sum256(value)
- _ = Set(bugKind, key, value)
+ data, err := json.Marshal(bug)
+ if err != nil {
+ panic(fmt.Sprintf("error marshalling bug %+v: %v", bug, err))
+ }
+
+ key := sha256.Sum256(data)
+ _ = Set(bugKind, key, data)
})
}
// BugReports returns a new unordered array of the contents
// of all cached bug reports produced by this executable.
-func BugReports() [][]byte {
+// It also returns the location of the cache directory
+// used by this process (or "" on initialization error).
+func BugReports() (string, []bug.Bug) {
+ // To test this logic, run:
+ // $ TEST_GOPLS_BUG=oops gopls stats # trigger a bug
+ // $ gopls stats # list the bugs
+
dir, err := getCacheDir()
if err != nil {
- return nil // ignore initialization errors
+ return "", nil // ignore initialization errors
}
- var result [][]byte
- _ = filepath.Walk(filepath.Join(dir, bugKind),
- func(path string, info fs.FileInfo, err error) error {
- if err != nil {
- return nil // ignore readdir/stat errors
+ var result []bug.Bug
+ _ = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
+ if err != nil {
+ return nil // ignore readdir/stat errors
+ }
+ // Parse the key from each "XXXX-bug" cache file name.
+ if !info.IsDir() && strings.HasSuffix(path, bugKind) {
+ var key [32]byte
+ n, err := hex.Decode(key[:], []byte(filepath.Base(path)[:len(key)*2]))
+ if err != nil || n != len(key) {
+ return nil // ignore malformed file names
}
- if !info.IsDir() {
- var key [32]byte
- n, err := hex.Decode(key[:], []byte(filepath.Base(path)))
- if err != nil || n != len(key) {
- return nil // ignore malformed file names
+ content, err := Get(bugKind, key)
+ if err == nil { // ignore read errors
+ var b bug.Bug
+ if err := json.Unmarshal(content, &b); err != nil {
+ log.Printf("error marshalling bug %q: %v", string(content), err)
}
- content, err := Get(bugKind, key)
- if err == nil { // ignore read errors
- result = append(result, content)
- }
+ result = append(result, b)
}
- return nil
- })
- return result
+ }
+ return nil
+ })
+ return dir, result
}
diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go
index 9d12f97..7486f24 100644
--- a/gopls/internal/lsp/general.go
+++ b/gopls/internal/lsp/general.go
@@ -8,6 +8,7 @@
"context"
"encoding/json"
"fmt"
+ "go/build"
"log"
"os"
"path"
@@ -59,7 +60,7 @@
if err := s.handleOptionResults(ctx, source.SetOptions(options, params.InitializationOptions)); err != nil {
return nil, err
}
- options.ForClientCapabilities(params.Capabilities)
+ options.ForClientCapabilities(params.ClientInfo, params.Capabilities)
if options.ShowBugReports {
// Report the next bug that occurs on the server.
@@ -239,14 +240,16 @@
// GoVersionTable maps Go versions to the gopls version in which support will
// be deprecated, and the final gopls version supporting them without warnings.
-// Keep this in sync with gopls/README.md
+// Keep this in sync with gopls/README.md.
//
// Must be sorted in ascending order of Go version.
//
// Mutable for testing.
var GoVersionTable = []GoVersionSupport{
{12, "", "v0.7.5"},
- {15, "v0.11.0", "v0.9.5"},
+ {15, "", "v0.9.5"},
+ {16, "v0.13.0", "v0.11.0"},
+ {17, "v0.13.0", "v0.11.0"},
}
// GoVersionSupport holds information about end-of-life Go version support.
@@ -262,11 +265,13 @@
return GoVersionTable[len(GoVersionTable)-1].GoVersion + 1
}
-// versionMessage returns the warning/error message to display if the user is
-// on the given Go version, if any. The goVersion variable is the X in Go 1.X.
+// versionMessage returns the warning/error message to display if the user has
+// the given Go version, if any. The goVersion variable is the X in Go 1.X. If
+// fromBuild is set, the Go version is the version used to build gopls.
+// Otherwise, it is the go command version.
//
// If goVersion is invalid (< 0), it returns "", 0.
-func versionMessage(goVersion int) (string, protocol.MessageType) {
+func versionMessage(goVersion int, fromBuild bool) (string, protocol.MessageType) {
if goVersion < 0 {
return "", 0
}
@@ -276,7 +281,11 @@
var msgBuilder strings.Builder
mType := protocol.Error
- fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion)
+ if fromBuild {
+ fmt.Fprintf(&msgBuilder, "Gopls was built with Go version 1.%d", goVersion)
+ } else {
+ fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion)
+ }
if v.DeprecatedVersion != "" {
// not deprecated yet, just a warning
fmt.Fprintf(&msgBuilder, ", which will be unsupported by gopls %s. ", v.DeprecatedVersion)
@@ -299,15 +308,15 @@
//
// It should be called after views change.
func (s *Server) checkViewGoVersions() {
- oldestVersion := -1
+ oldestVersion, fromBuild := go1Point(), true
for _, view := range s.session.Views() {
viewVersion := view.GoVersion()
if oldestVersion == -1 || viewVersion < oldestVersion {
- oldestVersion = viewVersion
+ oldestVersion, fromBuild = viewVersion, false
}
}
- if msg, mType := versionMessage(oldestVersion); msg != "" {
+ if msg, mType := versionMessage(oldestVersion, fromBuild); msg != "" {
s.eventuallyShowMessage(context.Background(), &protocol.ShowMessageParams{
Type: mType,
Message: msg,
@@ -315,6 +324,21 @@
}
}
+// go1Point returns the x in Go 1.x. If an error occurs extracting the go
+// version, it returns -1.
+//
+// Copied from the testenv package.
+func go1Point() int {
+ for i := len(build.Default.ReleaseTags) - 1; i >= 0; i-- {
+ var version int
+ if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil {
+ continue
+ }
+ return version
+ }
+ return -1
+}
+
func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) error {
originalViews := len(s.session.Views())
viewErrors := make(map[span.URI]error)
@@ -445,14 +469,13 @@
// registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles
// registrations to the client and updates s.watchedDirectories.
+// The caller must not subsequently mutate patterns.
func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error {
if !s.session.Options().DynamicWatchedFilesSupported {
return nil
}
- for k := range s.watchedGlobPatterns {
- delete(s.watchedGlobPatterns, k)
- }
- watchers := []protocol.FileSystemWatcher{} // must be a slice
+ s.watchedGlobPatterns = patterns
+ watchers := make([]protocol.FileSystemWatcher, 0, len(patterns)) // must be a slice
val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate
for pattern := range patterns {
watchers = append(watchers, protocol.FileSystemWatcher{
@@ -473,10 +496,6 @@
return err
}
s.watchRegistrationCount++
-
- for k, v := range patterns {
- s.watchedGlobPatterns[k] = v
- }
return nil
}
diff --git a/gopls/internal/lsp/general_test.go b/gopls/internal/lsp/general_test.go
index a0312ba..6bc0dc1 100644
--- a/gopls/internal/lsp/general_test.go
+++ b/gopls/internal/lsp/general_test.go
@@ -14,18 +14,22 @@
func TestVersionMessage(t *testing.T) {
tests := []struct {
goVersion int
+ fromBuild bool
wantContains []string // string fragments that we expect to see
wantType protocol.MessageType
}{
- {-1, nil, 0},
- {12, []string{"1.12", "not supported", "upgrade to Go 1.16", "install gopls v0.7.5"}, protocol.Error},
- {13, []string{"1.13", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning},
- {15, []string{"1.15", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning},
- {16, nil, 0},
+ {-1, false, nil, 0},
+ {12, false, []string{"1.12", "not supported", "upgrade to Go 1.18", "install gopls v0.7.5"}, protocol.Error},
+ {13, false, []string{"1.13", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error},
+ {15, false, []string{"1.15", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error},
+ {15, true, []string{"Gopls was built with Go version 1.15", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error},
+ {16, false, []string{"1.16", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning},
+ {17, false, []string{"1.17", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning},
+ {17, true, []string{"Gopls was built with Go version 1.17", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning},
}
for _, test := range tests {
- gotMsg, gotType := versionMessage(test.goVersion)
+ gotMsg, gotType := versionMessage(test.goVersion, test.fromBuild)
if len(test.wantContains) == 0 && gotMsg != "" {
t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVersion, gotMsg)
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index af5fe38..cd1c85b 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -9,11 +9,14 @@
import (
"context"
"fmt"
+ "runtime"
"sort"
"strings"
+ "sync"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
+ "golang.org/x/sync/errgroup"
"golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -58,24 +61,36 @@
}
func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[span.URI][]*source.Diagnostic, error) {
+
+ g, ctx := errgroup.WithContext(ctx)
+ cpulimit := runtime.GOMAXPROCS(0)
+ g.SetLimit(cpulimit)
+
+ var mu sync.Mutex
reports := make(map[span.URI][]*source.Diagnostic)
+
for _, uri := range snapshot.ModFiles() {
- fh, err := snapshot.ReadFile(ctx, uri)
- if err != nil {
- return nil, err
- }
- reports[fh.URI()] = []*source.Diagnostic{}
- diagnostics, err := diagFn(ctx, snapshot, fh)
- if err != nil {
- return nil, err
- }
- for _, d := range diagnostics {
- fh, err := snapshot.ReadFile(ctx, d.URI)
+ uri := uri
+ g.Go(func() error {
+ fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
- return nil, err
+ return err
}
- reports[fh.URI()] = append(reports[fh.URI()], d)
- }
+ diagnostics, err := diagFn(ctx, snapshot, fh)
+ if err != nil {
+ return err
+ }
+ for _, d := range diagnostics {
+ mu.Lock()
+ reports[d.URI] = append(reports[fh.URI()], d)
+ mu.Unlock()
+ }
+ return nil
+ })
+ }
+
+ if err := g.Wait(); err != nil {
+ return nil, err
}
return reports, nil
}
diff --git a/gopls/internal/lsp/protocol/generate/tables.go b/gopls/internal/lsp/protocol/generate/tables.go
index 126301a..8fb9707 100644
--- a/gopls/internal/lsp/protocol/generate/tables.go
+++ b/gopls/internal/lsp/protocol/generate/tables.go
@@ -68,6 +68,7 @@
{"Command", "arguments"}: "[]json.RawMessage",
{"CompletionItem", "textEdit"}: "TextEdit",
{"Diagnostic", "code"}: "interface{}",
+ {"Diagnostic", "data"}: "json.RawMessage", // delay unmarshalling quickfixes
{"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}",
diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go
index 8469aeb..f8ebb46 100644
--- a/gopls/internal/lsp/protocol/tsprotocol.go
+++ b/gopls/internal/lsp/protocol/tsprotocol.go
@@ -896,7 +896,7 @@
// notification and `textDocument/codeAction` request.
//
// @since 3.16.0
- Data interface{} `json:"data,omitempty"`
+ Data *json.RawMessage `json:"data,omitempty"`
}
// Client capabilities specific to diagnostic pull requests.
diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go
index 9d9f023..a770616 100644
--- a/gopls/internal/lsp/regtest/expectation.go
+++ b/gopls/internal/lsp/regtest/expectation.go
@@ -235,7 +235,7 @@
}
return Expectation{
Check: check,
- Description: "received ShowMessage",
+ Description: fmt.Sprintf("received window/showMessage containing %q", containing),
}
}
@@ -576,50 +576,6 @@
return jsonProperty(m[path[0]], path[1:]...)
}
-// RegistrationMatching asserts that the client has received a capability
-// registration matching the given regexp.
-//
-// TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited.
-//
-// Deprecated: use (No)FileWatchMatching
-func RegistrationMatching(re string) Expectation {
- rec := regexp.MustCompile(re)
- check := func(s State) Verdict {
- for _, p := range s.registrations {
- for _, r := range p.Registrations {
- if rec.Match([]byte(r.Method)) {
- return Met
- }
- }
- }
- return Unmet
- }
- return Expectation{
- Check: check,
- Description: fmt.Sprintf("registration matching %q", re),
- }
-}
-
-// UnregistrationMatching asserts that the client has received an
-// unregistration whose ID matches the given regexp.
-func UnregistrationMatching(re string) Expectation {
- rec := regexp.MustCompile(re)
- check := func(s State) Verdict {
- for _, p := range s.unregistrations {
- for _, r := range p.Unregisterations {
- if rec.Match([]byte(r.Method)) {
- return Met
- }
- }
- }
- return Unmet
- }
- return Expectation{
- Check: check,
- Description: fmt.Sprintf("unregistration matching %q", re),
- }
-}
-
// Diagnostics asserts that there is at least one diagnostic matching the given
// filters.
func Diagnostics(filters ...DiagnosticFilter) Expectation {
diff --git a/gopls/internal/lsp/regtest/options.go b/gopls/internal/lsp/regtest/options.go
index 7a41696..f55fd5b 100644
--- a/gopls/internal/lsp/regtest/options.go
+++ b/gopls/internal/lsp/regtest/options.go
@@ -64,8 +64,14 @@
})
}
-// Settings is a RunOption that sets user-provided configuration for the LSP
-// server.
+// ClientName sets the LSP client name.
+func ClientName(name string) RunOption {
+ return optionSetter(func(opts *runConfig) {
+ opts.editor.ClientName = name
+ })
+}
+
+// Settings sets user-provided configuration for the LSP server.
//
// As a special case, the env setting must not be provided via Settings: use
// EnvVars instead.
diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go
index 9f82e90..db69565 100644
--- a/gopls/internal/lsp/server.go
+++ b/gopls/internal/lsp/server.go
@@ -29,7 +29,7 @@
return &Server{
diagnostics: map[span.URI]*fileReports{},
gcOptimizationDetails: make(map[source.PackageID]struct{}),
- watchedGlobPatterns: make(map[string]struct{}),
+ watchedGlobPatterns: nil, // empty
changedFiles: make(map[span.URI]struct{}),
session: session,
client: client,
@@ -85,6 +85,7 @@
// watchedGlobPatterns is the set of glob patterns that we have requested
// the client watch on disk. It will be updated as the set of directories
// that the server should watch changes.
+ // The map field may be reassigned but the map is immutable.
watchedGlobPatternsMu sync.Mutex
watchedGlobPatterns map[string]struct{}
watchRegistrationCount int
diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go
index 281772b..f777fdb 100644
--- a/gopls/internal/lsp/source/api_json.go
+++ b/gopls/internal/lsp/source/api_json.go
@@ -760,7 +760,7 @@
Command: "gopls.remove_dependency",
Title: "Remove a dependency",
Doc: "Removes a dependency from the go.mod file of a module.",
- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t\"OnlyDiagnostic\": bool,\n}",
+ ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t// If the module is tidied apart from the one unused diagnostic, we can\n\t// run `go get module@none`, and then run `go mod tidy`. Otherwise, we\n\t// must make textual edits.\n\t\"OnlyDiagnostic\": bool,\n}",
},
{
Command: "gopls.reset_go_mod_diagnostics",
diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go
index ad5ce16..bc2b0c3 100644
--- a/gopls/internal/lsp/source/completion/completion.go
+++ b/gopls/internal/lsp/source/completion/completion.go
@@ -12,6 +12,7 @@
"go/ast"
"go/constant"
"go/parser"
+ "go/printer"
"go/scanner"
"go/token"
"go/types"
@@ -1268,19 +1269,30 @@
var sn snippet.Builder
sn.WriteText(id.Name)
sn.WriteText("(")
+
+ var cfg printer.Config // slight overkill
var nparams int
- for _, field := range fn.Type.Params.List {
- if field.Names != nil {
- nparams += len(field.Names)
- } else {
- nparams++
- }
- }
- for i := 0; i < nparams; i++ {
- if i > 0 {
+ param := func(name string, typ ast.Expr) {
+ if nparams > 0 {
sn.WriteText(", ")
}
- sn.WritePlaceholder(nil)
+ nparams++
+ sn.WritePlaceholder(func(b *snippet.Builder) {
+ var buf strings.Builder
+ buf.WriteString(name)
+ buf.WriteByte(' ')
+ cfg.Fprint(&buf, token.NewFileSet(), typ)
+ b.WriteText(buf.String())
+ })
+ }
+ for _, field := range fn.Type.Params.List {
+ if field.Names != nil {
+ for _, name := range field.Names {
+ param(name.Name, field.Type)
+ }
+ } else {
+ param("_", field.Type)
+ }
}
sn.WriteText(")")
item.snippet = &sn
diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go
index fc08dcf..336e35b 100644
--- a/gopls/internal/lsp/source/diagnostics.go
+++ b/gopls/internal/lsp/source/diagnostics.go
@@ -6,7 +6,9 @@
import (
"context"
+ "encoding/json"
+ "golang.org/x/tools/gopls/internal/bug"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/span"
)
@@ -136,3 +138,81 @@
*outT = append(*outT, tdiags...)
}
+
+// quickFixesJSON is a JSON-serializable list of quick fixes
+// to be saved in the protocol.Diagnostic.Data field.
+type quickFixesJSON struct {
+ // TODO(rfindley): pack some sort of identifier here for later
+ // lookup/validation?
+ Fixes []protocol.CodeAction
+}
+
+// BundleQuickFixes attempts to bundle sd.SuggestedFixes into the
+// sd.BundledFixes field, so that it can be round-tripped through the client.
+// It returns false if the quick-fixes cannot be bundled.
+func BundleQuickFixes(sd *Diagnostic) bool {
+ if len(sd.SuggestedFixes) == 0 {
+ return true
+ }
+ var actions []protocol.CodeAction
+ for _, fix := range sd.SuggestedFixes {
+ if fix.Edits != nil {
+ // For now, we only support bundled code actions that execute commands.
+ //
+ // In order to cleanly support bundled edits, we'd have to guarantee that
+ // the edits were generated on the current snapshot. But this naively
+ // implies that every fix would have to include a snapshot ID, which
+ // would require us to republish all diagnostics on each new snapshot.
+ //
+ // TODO(rfindley): in order to avoid this additional chatter, we'd need
+ // to build some sort of registry or other mechanism on the snapshot to
+ // check whether a diagnostic is still valid.
+ return false
+ }
+ action := protocol.CodeAction{
+ Title: fix.Title,
+ Kind: fix.ActionKind,
+ Command: fix.Command,
+ }
+ actions = append(actions, action)
+ }
+ fixes := quickFixesJSON{
+ Fixes: actions,
+ }
+ data, err := json.Marshal(fixes)
+ if err != nil {
+ bug.Reportf("marshalling quick fixes: %v", err)
+ return false
+ }
+ msg := json.RawMessage(data)
+ sd.BundledFixes = &msg
+ return true
+}
+
+// BundledQuickFixes extracts any bundled codeActions from the
+// diag.Data field.
+func BundledQuickFixes(diag protocol.Diagnostic) []protocol.CodeAction {
+ if diag.Data == nil {
+ return nil
+ }
+ var fix quickFixesJSON
+ if err := json.Unmarshal(*diag.Data, &fix); err != nil {
+ bug.Reportf("unmarshalling quick fix: %v", err)
+ return nil
+ }
+
+ var actions []protocol.CodeAction
+ for _, action := range fix.Fixes {
+ // See BundleQuickFixes: for now we only support bundling commands.
+ if action.Edit != nil {
+ bug.Reportf("bundled fix %q includes workspace edits", action.Title)
+ continue
+ }
+ // associate the action with the incoming diagnostic
+ // (Note that this does not mutate the fix.Fixes slice).
+ action.Diagnostics = []protocol.Diagnostic{diag}
+ actions = append(actions, action)
+ }
+
+ return actions
+}
diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go
index 2ca8895..23d6e9a 100644
--- a/gopls/internal/lsp/source/options.go
+++ b/gopls/internal/lsp/source/options.go
@@ -169,6 +169,7 @@
DeepCompletion: true,
ChattyDiagnostics: true,
NewDiff: "both",
+ SubdirWatchPatterns: SubdirWatchPatternsAuto,
},
Hooks: Hooks{
// TODO(adonovan): switch to new diff.Strings implementation.
@@ -198,6 +199,7 @@
// ClientOptions holds LSP-specific configuration that is provided by the
// client.
type ClientOptions struct {
+ ClientInfo *protocol.Msg_XInitializeParams_clientInfo
InsertTextFormat protocol.InsertTextFormat
ConfigurationSupported bool
DynamicConfigurationSupported bool
@@ -536,6 +538,9 @@
// 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
// by the user.
+//
+// TODO(rfindley): even though these settings are not intended for
+// modification, we should surface them in our documentation.
type InternalOptions struct {
// LiteralCompletions controls whether literal candidates such as
// "&someStruct{}" are offered. Tests disable this flag to simplify
@@ -599,8 +604,42 @@
// file change. If unset, gopls only reports diagnostics when they change, or
// when a file is opened or closed.
ChattyDiagnostics bool
+
+ // SubdirWatchPatterns configures the file watching glob patterns registered
+ // by gopls.
+ //
+ // Some clients (namely VS Code) do not send workspace/didChangeWatchedFile
+ // notifications for files contained in a directory when that directory is
+ // deleted:
+ // https://github.com/microsoft/vscode/issues/109754
+ //
+ // In this case, gopls would miss important notifications about deleted
+ // packages. To work around this, gopls registers a watch pattern for each
+ // directory containing Go files.
+ //
+ // Unfortunately, other clients experience performance problems with this
+ // many watch patterns, so there is no single behavior that works well for
+ // all clients.
+ //
+ // The "subdirWatchPatterns" setting allows configuring this behavior. Its
+ // default value of "auto" attempts to guess the correct behavior based on
+ // the client name. We'd love to avoid this specialization, but as described
+ // above there is no single value that works for all clients.
+ //
+ // If any LSP client does not behave well with the default value (for
+ // example, if like VS Code it drops file notifications), please file an
+ // issue.
+ SubdirWatchPatterns SubdirWatchPatterns
}
+type SubdirWatchPatterns string
+
+const (
+ SubdirWatchPatternsOn SubdirWatchPatterns = "on"
+ SubdirWatchPatternsOff SubdirWatchPatterns = "off"
+ SubdirWatchPatternsAuto SubdirWatchPatterns = "auto"
+)
+
type ImportShortcut string
const (
@@ -742,7 +781,8 @@
return results
}
-func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) {
+func (o *Options) ForClientCapabilities(clientName *protocol.Msg_XInitializeParams_clientInfo, caps protocol.ClientCapabilities) {
+ o.ClientInfo = clientName
// Check if the client supports snippets in completion items.
if caps.Workspace.WorkspaceEdit != nil {
o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations
@@ -1159,6 +1199,15 @@
case "chattyDiagnostics":
result.setBool(&o.ChattyDiagnostics)
+ case "subdirWatchPatterns":
+ if s, ok := result.asOneOf(
+ string(SubdirWatchPatternsOn),
+ string(SubdirWatchPatternsOff),
+ string(SubdirWatchPatternsAuto),
+ ); ok {
+ o.SubdirWatchPatterns = SubdirWatchPatterns(s)
+ }
+
// Replaced settings.
case "experimentalDisabledAnalyses":
result.deprecated("analyses")
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 6dd3811..2f7a3f2 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -8,6 +8,7 @@
"bytes"
"context"
"crypto/sha256"
+ "encoding/json"
"errors"
"fmt"
"go/ast"
@@ -971,7 +972,18 @@
Related []protocol.DiagnosticRelatedInformation
// Fields below are used internally to generate quick fixes. They aren't
- // part of the LSP spec and don't leave the server.
+ // part of the LSP spec and historically didn't leave the server.
+ //
+ // Update(2023-05): version 3.16 of the LSP spec included support for the
+ // Diagnostic.data field, which holds arbitrary data preserved in the
+ // diagnostic for codeAction requests. This field allows bundling additional
+ // information for quick-fixes, and gopls can (and should) use this
+ // information to avoid re-evaluating diagnostics in code-action handlers.
+ //
+ // In order to stage this transition incrementally, the 'BundledFixes' field
+ // may store a 'bundled' (=json-serialized) form of the associated
+ // SuggestedFixes. Not all diagnostics have their fixes bundled.
+ BundledFixes *json.RawMessage
SuggestedFixes []SuggestedFix
}
diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go
index 6bde10e..2030f32 100644
--- a/gopls/internal/regtest/bench/didchange_test.go
+++ b/gopls/internal/regtest/bench/didchange_test.go
@@ -19,11 +19,13 @@
// shared file cache.
var editID int64 = time.Now().UnixNano()
-var didChangeTests = []struct {
+type changeTest struct {
repo string
file string
-}{
- {"google-cloud-go", "httpreplay/httpreplay.go"},
+}
+
+var didChangeTests = []changeTest{
+ {"google-cloud-go", "internal/annotate.go"},
{"istio", "pkg/fuzz/util.go"},
{"kubernetes", "pkg/controller/lookup_cache.go"},
{"kuma", "api/generic/insights.go"},
@@ -64,43 +66,63 @@
func BenchmarkDiagnoseChange(b *testing.B) {
for _, test := range didChangeTests {
- b.Run(test.repo, func(b *testing.B) {
- sharedEnv := getRepo(b, test.repo).sharedEnv(b)
- config := fake.EditorConfig{
- Env: map[string]string{
- "GOPATH": sharedEnv.Sandbox.GOPATH(),
- },
- Settings: map[string]interface{}{
- "diagnosticsDelay": "0s",
- },
- }
- // Use a new env to avoid the diagnostic delay: we want to measure how
- // long it takes to produce the diagnostics.
- env := getRepo(b, test.repo).newEnv(b, "diagnoseChange", config)
- defer env.Close()
- env.OpenFile(test.file)
- // Insert the text we'll be modifying at the top of the file.
- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"})
- env.AfterChange()
- b.ResetTimer()
-
- // We must use an extra subtest layer here, so that we only set up the
- // shared env once (otherwise we pay additional overhead and the profiling
- // flags don't work).
- b.Run("diagnose", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
- edits := atomic.AddInt64(&editID, 1)
- env.EditBuffer(test.file, protocol.TextEdit{
- Range: protocol.Range{
- Start: protocol.Position{Line: 0, Character: 0},
- End: protocol.Position{Line: 1, Character: 0},
- },
- // Increment the placeholder text, to ensure cache misses.
- NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
- })
- env.AfterChange()
- }
- })
- })
+ runChangeDiagnosticsBenchmark(b, test, false)
}
}
+
+// TODO(rfindley): add a benchmark for with a metadata-affecting change, when
+// this matters.
+func BenchmarkDiagnoseSave(b *testing.B) {
+ for _, test := range didChangeTests {
+ runChangeDiagnosticsBenchmark(b, test, true)
+ }
+}
+
+// runChangeDiagnosticsBenchmark runs a benchmark to edit the test file and
+// await the resulting diagnostics pass. If save is set, the file is also saved.
+func runChangeDiagnosticsBenchmark(b *testing.B, test changeTest, save bool) {
+ b.Run(test.repo, func(b *testing.B) {
+ sharedEnv := getRepo(b, test.repo).sharedEnv(b)
+ config := fake.EditorConfig{
+ Env: map[string]string{
+ "GOPATH": sharedEnv.Sandbox.GOPATH(),
+ },
+ Settings: map[string]interface{}{
+ "diagnosticsDelay": "0s",
+ },
+ }
+ // Use a new env to avoid the diagnostic delay: we want to measure how
+ // long it takes to produce the diagnostics.
+ env := getRepo(b, test.repo).newEnv(b, "diagnoseSave", config)
+ defer env.Close()
+ env.OpenFile(test.file)
+ // Insert the text we'll be modifying at the top of the file.
+ env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"})
+ if save {
+ env.SaveBuffer(test.file)
+ }
+ env.AfterChange()
+ b.ResetTimer()
+
+ // We must use an extra subtest layer here, so that we only set up the
+ // shared env once (otherwise we pay additional overhead and the profiling
+ // flags don't work).
+ b.Run("diagnose", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ edits := atomic.AddInt64(&editID, 1)
+ env.EditBuffer(test.file, protocol.TextEdit{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 0, Character: 0},
+ End: protocol.Position{Line: 1, Character: 0},
+ },
+ // Increment the placeholder text, to ensure cache misses.
+ NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
+ })
+ if save {
+ env.SaveBuffer(test.file)
+ }
+ env.AfterChange()
+ }
+ })
+ })
+}
diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go
index 872bdc2..7f86594 100644
--- a/gopls/internal/regtest/completion/completion_test.go
+++ b/gopls/internal/regtest/completion/completion_test.go
@@ -527,13 +527,60 @@
env.AcceptCompletion(loc, completions.Items[0])
env.Await(env.DoneWithChange())
got := env.BufferText("main.go")
- want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:})\r\n}\r\n"
+ want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:x float64})\r\n}\r\n"
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unimported completion (-want +got):\n%s", diff)
}
})
}
+func TestUnimportedCompletionHasPlaceholders60269(t *testing.T) {
+ // We can't express this as a marker test because it doesn't support AcceptCompletion.
+ const src = `
+-- go.mod --
+module example.com
+go 1.12
+
+-- a/a.go --
+package a
+
+var _ = b.F
+
+-- b/b.go --
+package b
+
+func F0(a, b int, c float64) {}
+func F1(int, chan *string) {}
+`
+ WithOptions(
+ WindowsLineEndings(),
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+ env.Await(env.DoneWithOpen())
+
+ // The table lists the expected completions as they appear in Items.
+ const common = "package a\r\n\r\nimport \"example.com/b\"\r\n\r\nvar _ = "
+ for i, want := range []string{
+ common + "b.F0(${1:a int}, ${2:b int}, ${3:c float64})\r\n",
+ common + "b.F1(${1:_ int}, ${2:_ chan *string})\r\n",
+ } {
+ loc := env.RegexpSearch("a/a.go", "b.F()")
+ completions := env.Completion(loc)
+ if len(completions.Items) == 0 {
+ t.Fatalf("no completion items")
+ }
+ saved := env.BufferText("a/a.go")
+ env.AcceptCompletion(loc, completions.Items[i])
+ env.Await(env.DoneWithChange())
+ got := env.BufferText("a/a.go")
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("%d: unimported completion (-want +got):\n%s", i, diff)
+ }
+ env.SetBufferContent("a/a.go", saved) // restore
+ }
+ })
+}
+
func TestPackageMemberCompletionAfterSyntaxError(t *testing.T) {
// This test documents the current broken behavior due to golang/go#58833.
const src = `
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index f8e59a0..de675a5 100644
--- a/gopls/internal/regtest/diagnostics/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -1506,7 +1506,13 @@
bob.Hello()
}
`
- Run(t, mod, func(t *testing.T, env *Env) {
+ WithOptions(
+ Settings{
+ // Now that we don't watch subdirs by default (except for VS Code),
+ // we must explicitly ask gopls to requests subdir watch patterns.
+ "subdirWatchPatterns": "on",
+ },
+ ).Run(t, mod, func(t *testing.T, env *Env) {
env.OnceMet(
InitialWorkspaceLoad,
FileWatchMatching("bob"),
@@ -1695,8 +1701,7 @@
env.OpenFile("nested/hello/hello.go")
env.AfterChange(
Diagnostics(env.AtRegexp("nested/hello/hello.go", "helloHelper")),
- Diagnostics(env.AtRegexp("nested/hello/hello.go", "package hello"), WithMessage("nested module")),
- OutstandingWork(lsp.WorkspaceLoadFailure, "nested module"),
+ Diagnostics(env.AtRegexp("nested/hello/hello.go", "package (hello)"), WithMessage("not included in your workspace")),
)
})
}
diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go
index 6cbfe37..853abcd 100644
--- a/gopls/internal/regtest/misc/configuration_test.go
+++ b/gopls/internal/regtest/misc/configuration_test.go
@@ -57,9 +57,7 @@
//
// Gopls should not get confused about buffer content when recreating the view.
func TestMajorOptionsChange(t *testing.T) {
- t.Skip("broken due to golang/go#57934")
-
- testenv.NeedsGo1Point(t, 17)
+ testenv.NeedsGo1Point(t, 19) // needs staticcheck
const files = `
-- go.mod --
diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go
index 42aa372..b5da9b0 100644
--- a/gopls/internal/regtest/misc/failures_test.go
+++ b/gopls/internal/regtest/misc/failures_test.go
@@ -15,7 +15,6 @@
// that includes a line directive, which makes no difference since
// gopls ignores line directives.
func TestHoverFailure(t *testing.T) {
- t.Skip("line directives //line ")
const mod = `
-- go.mod --
module mod.com
@@ -48,7 +47,6 @@
// This test demonstrates a case where gopls is not at all confused by
// line directives, because it completely ignores them.
func TestFailingDiagnosticClearingOnEdit(t *testing.T) {
- t.Skip("line directives //line ")
// badPackageDup contains a duplicate definition of the 'a' const.
// This is a minor variant of TestDiagnosticClearingOnEdit from
// diagnostics_test.go, with a line directive, which makes no difference.
diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go
index 24ee6d8..41c6529 100644
--- a/gopls/internal/regtest/misc/hover_test.go
+++ b/gopls/internal/regtest/misc/hover_test.go
@@ -84,12 +84,6 @@
}
func TestHoverIntLiteral(t *testing.T) {
- // TODO(rfindley): this behavior doesn't actually make sense for vars. It is
- // misleading to format their value when it is (of course) variable.
- //
- // Instead, we should allow hovering on numeric literals.
- t.Skip("golang/go#58220: broken due to new hover logic")
-
const source = `
-- main.go --
package main
@@ -106,13 +100,13 @@
Run(t, source, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
hexExpected := "58190"
- got, _ := env.Hover(env.RegexpSearch("main.go", "hex"))
+ got, _ := env.Hover(env.RegexpSearch("main.go", "0xe"))
if got != nil && !strings.Contains(got.Value, hexExpected) {
t.Errorf("Hover: missing expected field '%s'. Got:\n%q", hexExpected, got.Value)
}
binExpected := "73"
- got, _ = env.Hover(env.RegexpSearch("main.go", "bigBin"))
+ got, _ = env.Hover(env.RegexpSearch("main.go", "0b1"))
if got != nil && !strings.Contains(got.Value, binExpected) {
t.Errorf("Hover: missing expected field '%s'. Got:\n%q", binExpected, got.Value)
}
diff --git a/gopls/internal/regtest/misc/leak_test.go b/gopls/internal/regtest/misc/leak_test.go
deleted file mode 100644
index 586ffcc..0000000
--- a/gopls/internal/regtest/misc/leak_test.go
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright 2022 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package misc
-
-import (
- "context"
- "testing"
-
- "github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/debug"
- "golang.org/x/tools/gopls/internal/lsp/fake"
- "golang.org/x/tools/gopls/internal/lsp/lsprpc"
- . "golang.org/x/tools/gopls/internal/lsp/regtest"
- "golang.org/x/tools/internal/jsonrpc2"
- "golang.org/x/tools/internal/jsonrpc2/servertest"
-)
-
-// Test for golang/go#57222.
-func TestCacheLeak(t *testing.T) {
- // TODO(rfindley): either fix this test with additional instrumentation, or
- // delete it.
- t.Skip("This test races with cache eviction.")
- const files = `-- a.go --
-package a
-
-func _() {
- println("1")
-}
-`
- c := cache.New(nil)
- env := setupEnv(t, files, c)
- env.Await(InitialWorkspaceLoad)
- env.OpenFile("a.go")
-
- // Make a couple edits to stabilize cache state.
- //
- // For some reason, after only one edit we're left with two parsed files
- // (perhaps because something had to ParseHeader). If this test proves flaky,
- // we'll need to investigate exactly what is causing various parse modes to
- // be present (or rewrite the test to be more tolerant, for example make ~100
- // modifications and assert that we're within a few of where we're started).
- env.RegexpReplace("a.go", "1", "2")
- env.RegexpReplace("a.go", "2", "3")
- env.AfterChange()
-
- // Capture cache state, make an arbitrary change, and wait for gopls to do
- // its work. Afterward, we should have the exact same number of parsed
- before := c.MemStats()
- env.RegexpReplace("a.go", "3", "4")
- env.AfterChange()
- after := c.MemStats()
-
- if diff := cmp.Diff(before, after); diff != "" {
- t.Errorf("store objects differ after change (-before +after)\n%s", diff)
- }
-}
-
-// setupEnv creates a new sandbox environment for editing the txtar encoded
-// content of files. It uses a new gopls instance backed by the Cache c.
-func setupEnv(t *testing.T, files string, c *cache.Cache) *Env {
- ctx := debug.WithInstance(context.Background(), "", "off")
- server := lsprpc.NewStreamServer(c, false, hooks.Options)
- ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream)
- s, err := fake.NewSandbox(&fake.SandboxConfig{
- Files: fake.UnpackTxt(files),
- })
- if err != nil {
- t.Fatal(err)
- }
-
- a := NewAwaiter(s.Workdir)
- const skipApplyEdits = false
- editor, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(ctx, ts, a.Hooks(), skipApplyEdits)
- if err != nil {
- t.Fatal(err)
- }
-
- return &Env{
- T: t,
- Ctx: ctx,
- Editor: editor,
- Sandbox: s,
- Awaiter: a,
- }
-}
diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go
index 03e60ac..855141a 100644
--- a/gopls/internal/regtest/modfile/modfile_test.go
+++ b/gopls/internal/regtest/modfile/modfile_test.go
@@ -498,14 +498,8 @@
ReadDiagnostics("a/go.mod", &modDiags),
)
- // golang.go#57987: now that gopls is incremental, we must be careful where
- // we request diagnostics. We must design a simpler way to correlate
- // published diagnostics with subsequent code action requests (see also the
- // comment in Server.codeAction).
- const canRequestCodeActionsForWorkspaceDiagnostics = false
- if canRequestCodeActionsForWorkspaceDiagnostics {
- env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics)
- const want = `module mod.com
+ env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics)
+ const want = `module mod.com
go 1.12
@@ -514,11 +508,10 @@
example.com/blah/v2 v2.0.0
)
`
- env.SaveBuffer("a/go.mod")
- env.AfterChange(NoDiagnostics(ForFile("a/main.go")))
- if got := env.BufferText("a/go.mod"); got != want {
- t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got))
- }
+ env.SaveBuffer("a/go.mod")
+ env.AfterChange(NoDiagnostics(ForFile("a/main.go")))
+ if got := env.BufferText("a/go.mod"); got != want {
+ t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got))
}
})
}
diff --git a/gopls/internal/regtest/watch/setting_test.go b/gopls/internal/regtest/watch/setting_test.go
new file mode 100644
index 0000000..9ed7fde
--- /dev/null
+++ b/gopls/internal/regtest/watch/setting_test.go
@@ -0,0 +1,85 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package regtest
+
+import (
+ "fmt"
+ "testing"
+
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+)
+
+func TestSubdirWatchPatterns(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.test
+
+go 1.18
+-- subdir/subdir.go --
+package subdir
+`
+
+ tests := []struct {
+ clientName string
+ subdirWatchPatterns string
+ wantWatched bool
+ }{
+ {"other client", "on", true},
+ {"other client", "off", false},
+ {"other client", "auto", false},
+ {"Visual Studio Code", "auto", true},
+ }
+
+ for _, test := range tests {
+ t.Run(fmt.Sprintf("%s_%s", test.clientName, test.subdirWatchPatterns), func(t *testing.T) {
+ WithOptions(
+ ClientName(test.clientName),
+ Settings{
+ "subdirWatchPatterns": test.subdirWatchPatterns,
+ },
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ var expectation Expectation
+ if test.wantWatched {
+ expectation = FileWatchMatching("subdir")
+ } else {
+ expectation = NoFileWatchMatching("subdir")
+ }
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ expectation,
+ )
+ })
+ })
+ }
+}
+
+// This test checks that we surface errors for invalid subdir watch patterns,
+// as the triple of ("off"|"on"|"auto") may be confusing to users inclined to
+// use (true|false) or some other truthy value.
+func TestSubdirWatchPatterns_BadValues(t *testing.T) {
+ tests := []struct {
+ badValue interface{}
+ wantMessage string
+ }{
+ {true, "invalid type bool, expect string"},
+ {false, "invalid type bool, expect string"},
+ {"yes", `invalid option "yes"`},
+ }
+
+ for _, test := range tests {
+ t.Run(fmt.Sprint(test.badValue), func(t *testing.T) {
+ WithOptions(
+ Settings{
+ "subdirWatchPatterns": test.badValue,
+ },
+ ).Run(t, "", func(t *testing.T, env *Env) {
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ ShownMessage(test.wantMessage),
+ )
+ })
+ })
+ }
+}
diff --git a/gopls/internal/regtest/workspace/adhoc_test.go b/gopls/internal/regtest/workspace/adhoc_test.go
new file mode 100644
index 0000000..d726242
--- /dev/null
+++ b/gopls/internal/regtest/workspace/adhoc_test.go
@@ -0,0 +1,42 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package workspace
+
+import (
+ "testing"
+
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/internal/testenv"
+)
+
+// Test for golang/go#57209: editing a file in an ad-hoc package should not
+// trigger conflicting diagnostics.
+func TestAdhoc_Edits(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+
+ const files = `
+-- a.go --
+package foo
+
+const X = 1
+
+-- b.go --
+package foo
+
+// import "errors"
+
+const Y = X
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("b.go")
+
+ for i := 0; i < 10; i++ {
+ env.RegexpReplace("b.go", `// import "errors"`, `import "errors"`)
+ env.RegexpReplace("b.go", `import "errors"`, `// import "errors"`)
+ env.AfterChange(NoDiagnostics())
+ }
+ })
+}
diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go
index 005a7e9..d7d54c4 100644
--- a/gopls/internal/regtest/workspace/broken_test.go
+++ b/gopls/internal/regtest/workspace/broken_test.go
@@ -23,10 +23,9 @@
// Test for golang/go#53933
func TestBrokenWorkspace_DuplicateModules(t *testing.T) {
- testenv.NeedsGo1Point(t, 18)
-
- // TODO(golang/go#57650): fix this feature.
- t.Skip("we no longer detect duplicate modules")
+ // The go command error message was improved in Go 1.20 to mention multiple
+ // modules.
+ testenv.NeedsGo1Point(t, 20)
// This proxy module content is replaced by the workspace, but is still
// required for module resolution to function in the Go command.
@@ -98,8 +97,8 @@
ProxyFiles(proxy),
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("package1/main.go")
- env.Await(
- OutstandingWork(lsp.WorkspaceLoadFailure, `found module "example.com/foo" multiple times in the workspace`),
+ env.AfterChange(
+ OutstandingWork(lsp.WorkspaceLoadFailure, `module example.com/foo appears multiple times in workspace`),
)
// Remove the redundant vendored copy of example.com.
@@ -110,10 +109,10 @@
./package2/vendor/example.com/foo
)
`)
- env.Await(NoOutstandingWork())
+ env.AfterChange(NoOutstandingWork())
// Check that definitions in package1 go to the copy vendored in package2.
- location := env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI.SpanURI().Filename()
+ location := string(env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI)
const wantLocation = "package2/vendor/example.com/foo/foo.go"
if !strings.HasSuffix(location, wantLocation) {
t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation)
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index 5a94e42..02e3a8c 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -183,29 +183,6 @@
})
}
-// This test checks that gopls updates the set of files it watches when a
-// replace target is added to the go.mod.
-func TestWatchReplaceTargets(t *testing.T) {
- t.Skipf("skipping known-flaky test: see https://go.dev/issue/50748")
-
- WithOptions(
- ProxyFiles(workspaceProxy),
- WorkspaceFolders("pkg"),
- ).Run(t, workspaceModule, func(t *testing.T, env *Env) {
- // Add a replace directive and expect the files that gopls is watching
- // to change.
- dir := env.Sandbox.Workdir.URI("goodbye").SpanURI().Filename()
- goModWithReplace := fmt.Sprintf(`%s
-replace random.org => %s
-`, env.ReadWorkspaceFile("pkg/go.mod"), dir)
- env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace)
- env.AfterChange(
- UnregistrationMatching("didChangeWatchedFiles"),
- RegistrationMatching("didChangeWatchedFiles"),
- )
- })
-}
-
const workspaceModuleProxy = `
-- example.com@v1.2.3/go.mod --
module example.com
@@ -575,10 +552,18 @@
`
WithOptions(
ProxyFiles(workspaceModuleProxy),
+ Settings{
+ "subdirWatchPatterns": "on",
+ },
).Run(t, multiModule, func(t *testing.T, env *Env) {
- // Initially, the go.work should cause only the a.com module to be
- // loaded. Validate this by jumping to a definition in b.com and ensuring
- // that we go to the module cache.
+ // Initially, the go.work should cause only the a.com module to be loaded,
+ // so we shouldn't get any file watches for modb. Further validate this by
+ // jumping to a definition in b.com and ensuring that we go to the module
+ // cache.
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ NoFileWatchMatching("modb"),
+ )
env.OpenFile("moda/a/a.go")
env.Await(env.DoneWithOpen())
@@ -610,9 +595,13 @@
`)
// As of golang/go#54069, writing go.work to the workspace triggers a
- // workspace reload.
+ // workspace reload, and new file watches.
env.AfterChange(
Diagnostics(env.AtRegexp("modb/b/b.go", "x")),
+ // TODO(golang/go#60340): we don't get a file watch yet, because
+ // updateWatchedDirectories runs before snapshot.load. Instead, we get it
+ // after the next change (the didOpen below).
+ // FileWatchMatching("modb"),
)
// Jumping to definition should now go to b.com in the workspace.
@@ -623,7 +612,13 @@
// Now, let's modify the go.work *overlay* (not on disk), and verify that
// this change is only picked up once it is saved.
env.OpenFile("go.work")
- env.AfterChange()
+ env.AfterChange(
+ // TODO(golang/go#60340): delete this expectation in favor of
+ // the commented-out expectation above, once we fix the evaluation order
+ // of file watches. We should not have to wait for a second change to get
+ // the correct watches.
+ FileWatchMatching("modb"),
+ )
env.SetBufferContent("go.work", `go 1.17
use (
@@ -1059,7 +1054,7 @@
// package declaration.
env.AfterChange(
NoDiagnostics(ForFile("main.go")),
- Diagnostics(AtPosition("b/main.go", 0, 0)),
+ Diagnostics(env.AtRegexp("b/main.go", "package (main)")),
)
env.WriteWorkspaceFile("go.work", `go 1.16
@@ -1085,7 +1080,7 @@
env.AfterChange(
NoDiagnostics(ForFile("main.go")),
- Diagnostics(AtPosition("b/main.go", 0, 0)),
+ Diagnostics(env.AtRegexp("b/main.go", "package (main)")),
)
})
}
diff --git a/internal/lockedfile/internal/filelock/filelock.go b/internal/lockedfile/internal/filelock/filelock.go
deleted file mode 100644
index 05f27c3..0000000
--- a/internal/lockedfile/internal/filelock/filelock.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package filelock provides a platform-independent API for advisory file
-// locking. Calls to functions in this package on platforms that do not support
-// advisory locks will return errors for which IsNotSupported returns true.
-package filelock
-
-import (
- "errors"
- "io/fs"
- "os"
-)
-
-// A File provides the minimal set of methods required to lock an open file.
-// File implementations must be usable as map keys.
-// The usual implementation is *os.File.
-type File interface {
- // Name returns the name of the file.
- Name() string
-
- // Fd returns a valid file descriptor.
- // (If the File is an *os.File, it must not be closed.)
- Fd() uintptr
-
- // Stat returns the FileInfo structure describing file.
- Stat() (fs.FileInfo, error)
-}
-
-// Lock places an advisory write lock on the file, blocking until it can be
-// locked.
-//
-// If Lock returns nil, no other process will be able to place a read or write
-// lock on the file until this process exits, closes f, or calls Unlock on it.
-//
-// If f's descriptor is already read- or write-locked, the behavior of Lock is
-// unspecified.
-//
-// Closing the file may or may not release the lock promptly. Callers should
-// ensure that Unlock is always called when Lock succeeds.
-func Lock(f File) error {
- return lock(f, writeLock)
-}
-
-// RLock places an advisory read lock on the file, blocking until it can be locked.
-//
-// If RLock returns nil, no other process will be able to place a write lock on
-// the file until this process exits, closes f, or calls Unlock on it.
-//
-// If f is already read- or write-locked, the behavior of RLock is unspecified.
-//
-// Closing the file may or may not release the lock promptly. Callers should
-// ensure that Unlock is always called if RLock succeeds.
-func RLock(f File) error {
- return lock(f, readLock)
-}
-
-// Unlock removes an advisory lock placed on f by this process.
-//
-// The caller must not attempt to unlock a file that is not locked.
-func Unlock(f File) error {
- return unlock(f)
-}
-
-// String returns the name of the function corresponding to lt
-// (Lock, RLock, or Unlock).
-func (lt lockType) String() string {
- switch lt {
- case readLock:
- return "RLock"
- case writeLock:
- return "Lock"
- default:
- return "Unlock"
- }
-}
-
-// IsNotSupported returns a boolean indicating whether the error is known to
-// report that a function is not supported (possibly for a specific input).
-// It is satisfied by ErrNotSupported as well as some syscall errors.
-func IsNotSupported(err error) bool {
- return isNotSupported(underlyingError(err))
-}
-
-var ErrNotSupported = errors.New("operation not supported")
-
-// underlyingError returns the underlying error for known os error types.
-func underlyingError(err error) error {
- switch err := err.(type) {
- case *fs.PathError:
- return err.Err
- case *os.LinkError:
- return err.Err
- case *os.SyscallError:
- return err.Err
- }
- return err
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_fcntl.go b/internal/lockedfile/internal/filelock/filelock_fcntl.go
deleted file mode 100644
index 3098519..0000000
--- a/internal/lockedfile/internal/filelock/filelock_fcntl.go
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build aix || (solaris && !illumos)
-// +build aix solaris,!illumos
-
-// This code implements the filelock API using POSIX 'fcntl' locks, which attach
-// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
-// files prematurely when the same file is opened through different descriptors,
-// we allow only one read-lock at a time.
-//
-// Most platforms provide some alternative API, such as an 'flock' system call
-// or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and
-// does not require per-inode bookkeeping in the application.
-
-package filelock
-
-import (
- "errors"
- "io"
- "io/fs"
- "math/rand"
- "sync"
- "syscall"
- "time"
-)
-
-type lockType int16
-
-const (
- readLock lockType = syscall.F_RDLCK
- writeLock lockType = syscall.F_WRLCK
-)
-
-type inode = uint64 // type of syscall.Stat_t.Ino
-
-type inodeLock struct {
- owner File
- queue []<-chan File
-}
-
-var (
- mu sync.Mutex
- inodes = map[File]inode{}
- locks = map[inode]inodeLock{}
-)
-
-func lock(f File, lt lockType) (err error) {
- // POSIX locks apply per inode and process, and the lock for an inode is
- // released when *any* descriptor for that inode is closed. So we need to
- // synchronize access to each inode internally, and must serialize lock and
- // unlock calls that refer to the same inode through different descriptors.
- fi, err := f.Stat()
- if err != nil {
- return err
- }
- ino := fi.Sys().(*syscall.Stat_t).Ino
-
- mu.Lock()
- if i, dup := inodes[f]; dup && i != ino {
- mu.Unlock()
- return &fs.PathError{
- Op: lt.String(),
- Path: f.Name(),
- Err: errors.New("inode for file changed since last Lock or RLock"),
- }
- }
- inodes[f] = ino
-
- var wait chan File
- l := locks[ino]
- if l.owner == f {
- // This file already owns the lock, but the call may change its lock type.
- } else if l.owner == nil {
- // No owner: it's ours now.
- l.owner = f
- } else {
- // Already owned: add a channel to wait on.
- wait = make(chan File)
- l.queue = append(l.queue, wait)
- }
- locks[ino] = l
- mu.Unlock()
-
- if wait != nil {
- wait <- f
- }
-
- // Spurious EDEADLK errors arise on platforms that compute deadlock graphs at
- // the process, rather than thread, level. Consider processes P and Q, with
- // threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be
- // reported as a deadlock on systems that consider only process granularity:
- //
- // P.1 locks file A.
- // Q.3 locks file B.
- // Q.3 blocks on file A.
- // P.2 blocks on file B. (This is erroneously reported as a deadlock.)
- // P.1 unlocks file A.
- // Q.3 unblocks and locks file A.
- // Q.3 unlocks files A and B.
- // P.2 unblocks and locks file B.
- // P.2 unlocks file B.
- //
- // These spurious errors were observed in practice on AIX and Solaris in
- // cmd/go: see https://golang.org/issue/32817.
- //
- // We work around this bug by treating EDEADLK as always spurious. If there
- // really is a lock-ordering bug between the interacting processes, it will
- // become a livelock instead, but that's not appreciably worse than if we had
- // a proper flock implementation (which generally does not even attempt to
- // diagnose deadlocks).
- //
- // In the above example, that changes the trace to:
- //
- // P.1 locks file A.
- // Q.3 locks file B.
- // Q.3 blocks on file A.
- // P.2 spuriously fails to lock file B and goes to sleep.
- // P.1 unlocks file A.
- // Q.3 unblocks and locks file A.
- // Q.3 unlocks files A and B.
- // P.2 wakes up and locks file B.
- // P.2 unlocks file B.
- //
- // We know that the retry loop will not introduce a *spurious* livelock
- // because, according to the POSIX specification, EDEADLK is only to be
- // returned when “the lock is blocked by a lock from another process”.
- // If that process is blocked on some lock that we are holding, then the
- // resulting livelock is due to a real deadlock (and would manifest as such
- // when using, for example, the flock implementation of this package).
- // If the other process is *not* blocked on some other lock that we are
- // holding, then it will eventually release the requested lock.
-
- nextSleep := 1 * time.Millisecond
- const maxSleep = 500 * time.Millisecond
- for {
- err = setlkw(f.Fd(), lt)
- if err != syscall.EDEADLK {
- break
- }
- time.Sleep(nextSleep)
-
- nextSleep += nextSleep
- if nextSleep > maxSleep {
- nextSleep = maxSleep
- }
- // Apply 10% jitter to avoid synchronizing collisions when we finally unblock.
- nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
- }
-
- if err != nil {
- unlock(f)
- return &fs.PathError{
- Op: lt.String(),
- Path: f.Name(),
- Err: err,
- }
- }
-
- return nil
-}
-
-func unlock(f File) error {
- var owner File
-
- mu.Lock()
- ino, ok := inodes[f]
- if ok {
- owner = locks[ino].owner
- }
- mu.Unlock()
-
- if owner != f {
- panic("unlock called on a file that is not locked")
- }
-
- err := setlkw(f.Fd(), syscall.F_UNLCK)
-
- mu.Lock()
- l := locks[ino]
- if len(l.queue) == 0 {
- // No waiters: remove the map entry.
- delete(locks, ino)
- } else {
- // The first waiter is sending us their file now.
- // Receive it and update the queue.
- l.owner = <-l.queue[0]
- l.queue = l.queue[1:]
- locks[ino] = l
- }
- delete(inodes, f)
- mu.Unlock()
-
- return err
-}
-
-// setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd.
-func setlkw(fd uintptr, lt lockType) error {
- for {
- err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{
- Type: int16(lt),
- Whence: io.SeekStart,
- Start: 0,
- Len: 0, // All bytes.
- })
- if err != syscall.EINTR {
- return err
- }
- }
-}
-
-func isNotSupported(err error) bool {
- return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_other.go b/internal/lockedfile/internal/filelock/filelock_other.go
deleted file mode 100644
index cde868f..0000000
--- a/internal/lockedfile/internal/filelock/filelock_other.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build !(aix || darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd || solaris) && !plan9 && !windows
-// +build !aix,!darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!solaris,!plan9,!windows
-
-package filelock
-
-import "io/fs"
-
-type lockType int8
-
-const (
- readLock = iota + 1
- writeLock
-)
-
-func lock(f File, lt lockType) error {
- return &fs.PathError{
- Op: lt.String(),
- Path: f.Name(),
- Err: ErrNotSupported,
- }
-}
-
-func unlock(f File) error {
- return &fs.PathError{
- Op: "Unlock",
- Path: f.Name(),
- Err: ErrNotSupported,
- }
-}
-
-func isNotSupported(err error) bool {
- return err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_plan9.go b/internal/lockedfile/internal/filelock/filelock_plan9.go
deleted file mode 100644
index 908afb6..0000000
--- a/internal/lockedfile/internal/filelock/filelock_plan9.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build plan9
-// +build plan9
-
-package filelock
-
-import "io/fs"
-
-type lockType int8
-
-const (
- readLock = iota + 1
- writeLock
-)
-
-func lock(f File, lt lockType) error {
- return &fs.PathError{
- Op: lt.String(),
- Path: f.Name(),
- Err: ErrNotSupported,
- }
-}
-
-func unlock(f File) error {
- return &fs.PathError{
- Op: "Unlock",
- Path: f.Name(),
- Err: ErrNotSupported,
- }
-}
-
-func isNotSupported(err error) bool {
- return err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_test.go b/internal/lockedfile/internal/filelock/filelock_test.go
deleted file mode 100644
index 6c3f393..0000000
--- a/internal/lockedfile/internal/filelock/filelock_test.go
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris windows
-
-package filelock_test
-
-import (
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "testing"
- "time"
-
- "golang.org/x/tools/internal/lockedfile/internal/filelock"
-)
-
-func lock(t *testing.T, f *os.File) {
- t.Helper()
- err := filelock.Lock(f)
- t.Logf("Lock(fd %d) = %v", f.Fd(), err)
- if err != nil {
- t.Fail()
- }
-}
-
-func rLock(t *testing.T, f *os.File) {
- t.Helper()
- err := filelock.RLock(f)
- t.Logf("RLock(fd %d) = %v", f.Fd(), err)
- if err != nil {
- t.Fail()
- }
-}
-
-func unlock(t *testing.T, f *os.File) {
- t.Helper()
- err := filelock.Unlock(f)
- t.Logf("Unlock(fd %d) = %v", f.Fd(), err)
- if err != nil {
- t.Fail()
- }
-}
-
-func mustTempFile(t *testing.T) (f *os.File, remove func()) {
- t.Helper()
-
- base := filepath.Base(t.Name())
- f, err := os.CreateTemp("", base)
- if err != nil {
- t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err)
- }
- t.Logf("fd %d = %s", f.Fd(), f.Name())
-
- return f, func() {
- f.Close()
- os.Remove(f.Name())
- }
-}
-
-func mustOpen(t *testing.T, name string) *os.File {
- t.Helper()
-
- f, err := os.OpenFile(name, os.O_RDWR, 0)
- if err != nil {
- t.Fatalf("os.Open(%q) = %v", name, err)
- }
-
- t.Logf("fd %d = os.Open(%q)", f.Fd(), name)
- return f
-}
-
-const (
- quiescent = 10 * time.Millisecond
- probablyStillBlocked = 10 * time.Second
-)
-
-func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) {
- t.Helper()
-
- desc := fmt.Sprintf("%s(fd %d)", op, f.Fd())
-
- done := make(chan struct{})
- go func() {
- t.Helper()
- switch op {
- case "Lock":
- lock(t, f)
- case "RLock":
- rLock(t, f)
- default:
- panic("invalid op: " + op)
- }
- close(done)
- }()
-
- select {
- case <-done:
- t.Fatalf("%s unexpectedly did not block", desc)
- return nil
-
- case <-time.After(quiescent):
- t.Logf("%s is blocked (as expected)", desc)
- return func(t *testing.T) {
- t.Helper()
- select {
- case <-time.After(probablyStillBlocked):
- t.Fatalf("%s is unexpectedly still blocked", desc)
- case <-done:
- }
- }
- }
-}
-
-func TestLockExcludesLock(t *testing.T) {
- t.Parallel()
-
- f, remove := mustTempFile(t)
- defer remove()
-
- other := mustOpen(t, f.Name())
- defer other.Close()
-
- lock(t, f)
- lockOther := mustBlock(t, "Lock", other)
- unlock(t, f)
- lockOther(t)
- unlock(t, other)
-}
-
-func TestLockExcludesRLock(t *testing.T) {
- t.Parallel()
-
- f, remove := mustTempFile(t)
- defer remove()
-
- other := mustOpen(t, f.Name())
- defer other.Close()
-
- lock(t, f)
- rLockOther := mustBlock(t, "RLock", other)
- unlock(t, f)
- rLockOther(t)
- unlock(t, other)
-}
-
-func TestRLockExcludesOnlyLock(t *testing.T) {
- t.Parallel()
-
- f, remove := mustTempFile(t)
- defer remove()
- rLock(t, f)
-
- f2 := mustOpen(t, f.Name())
- defer f2.Close()
-
- doUnlockTF := false
- switch runtime.GOOS {
- case "aix", "solaris":
- // When using POSIX locks (as on Solaris), we can't safely read-lock the
- // same inode through two different descriptors at the same time: when the
- // first descriptor is closed, the second descriptor would still be open but
- // silently unlocked. So a second RLock must block instead of proceeding.
- lockF2 := mustBlock(t, "RLock", f2)
- unlock(t, f)
- lockF2(t)
- default:
- rLock(t, f2)
- doUnlockTF = true
- }
-
- other := mustOpen(t, f.Name())
- defer other.Close()
- lockOther := mustBlock(t, "Lock", other)
-
- unlock(t, f2)
- if doUnlockTF {
- unlock(t, f)
- }
- lockOther(t)
- unlock(t, other)
-}
-
-func TestLockNotDroppedByExecCommand(t *testing.T) {
- f, remove := mustTempFile(t)
- defer remove()
-
- lock(t, f)
-
- other := mustOpen(t, f.Name())
- defer other.Close()
-
- // Some kinds of file locks are dropped when a duplicated or forked file
- // descriptor is unlocked. Double-check that the approach used by os/exec does
- // not accidentally drop locks.
- cmd := exec.Command(os.Args[0], "-test.run=^$")
- if err := cmd.Run(); err != nil {
- t.Fatalf("exec failed: %v", err)
- }
-
- lockOther := mustBlock(t, "Lock", other)
- unlock(t, f)
- lockOther(t)
- unlock(t, other)
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_unix.go b/internal/lockedfile/internal/filelock/filelock_unix.go
deleted file mode 100644
index 878a1e7..0000000
--- a/internal/lockedfile/internal/filelock/filelock_unix.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
-// +build darwin dragonfly freebsd illumos linux netbsd openbsd
-
-package filelock
-
-import (
- "io/fs"
- "syscall"
-)
-
-type lockType int16
-
-const (
- readLock lockType = syscall.LOCK_SH
- writeLock lockType = syscall.LOCK_EX
-)
-
-func lock(f File, lt lockType) (err error) {
- for {
- err = syscall.Flock(int(f.Fd()), int(lt))
- if err != syscall.EINTR {
- break
- }
- }
- if err != nil {
- return &fs.PathError{
- Op: lt.String(),
- Path: f.Name(),
- Err: err,
- }
- }
- return nil
-}
-
-func unlock(f File) error {
- return lock(f, syscall.LOCK_UN)
-}
-
-func isNotSupported(err error) bool {
- return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_windows.go b/internal/lockedfile/internal/filelock/filelock_windows.go
deleted file mode 100644
index 3273a81..0000000
--- a/internal/lockedfile/internal/filelock/filelock_windows.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build windows
-// +build windows
-
-package filelock
-
-import (
- "io/fs"
-
- "golang.org/x/sys/windows"
-)
-
-type lockType uint32
-
-const (
- readLock lockType = 0
- writeLock lockType = windows.LOCKFILE_EXCLUSIVE_LOCK
-)
-
-const (
- reserved = 0
- allBytes = ^uint32(0)
-)
-
-func lock(f File, lt lockType) error {
- // Per https://golang.org/issue/19098, “Programs currently expect the Fd
- // method to return a handle that uses ordinary synchronous I/O.”
- // However, LockFileEx still requires an OVERLAPPED structure,
- // which contains the file offset of the beginning of the lock range.
- // We want to lock the entire file, so we leave the offset as zero.
- ol := new(windows.Overlapped)
-
- err := windows.LockFileEx(windows.Handle(f.Fd()), uint32(lt), reserved, allBytes, allBytes, ol)
- if err != nil {
- return &fs.PathError{
- Op: lt.String(),
- Path: f.Name(),
- Err: err,
- }
- }
- return nil
-}
-
-func unlock(f File) error {
- ol := new(windows.Overlapped)
- err := windows.UnlockFileEx(windows.Handle(f.Fd()), reserved, allBytes, allBytes, ol)
- if err != nil {
- return &fs.PathError{
- Op: "Unlock",
- Path: f.Name(),
- Err: err,
- }
- }
- return nil
-}
-
-func isNotSupported(err error) bool {
- switch err {
- case windows.ERROR_NOT_SUPPORTED, windows.ERROR_CALL_NOT_IMPLEMENTED, ErrNotSupported:
- return true
- default:
- return false
- }
-}
diff --git a/internal/lockedfile/lockedfile.go b/internal/lockedfile/lockedfile.go
deleted file mode 100644
index 82e1a89..0000000
--- a/internal/lockedfile/lockedfile.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package lockedfile creates and manipulates files whose contents should only
-// change atomically.
-package lockedfile
-
-import (
- "fmt"
- "io"
- "io/fs"
- "os"
- "runtime"
-)
-
-// A File is a locked *os.File.
-//
-// Closing the file releases the lock.
-//
-// If the program exits while a file is locked, the operating system releases
-// the lock but may not do so promptly: callers must ensure that all locked
-// files are closed before exiting.
-type File struct {
- osFile
- closed bool
-}
-
-// osFile embeds a *os.File while keeping the pointer itself unexported.
-// (When we close a File, it must be the same file descriptor that we opened!)
-type osFile struct {
- *os.File
-}
-
-// OpenFile is like os.OpenFile, but returns a locked file.
-// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked;
-// otherwise, it is read-locked.
-func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
- var (
- f = new(File)
- err error
- )
- f.osFile.File, err = openFile(name, flag, perm)
- if err != nil {
- return nil, err
- }
-
- // Although the operating system will drop locks for open files when the go
- // command exits, we want to hold locks for as little time as possible, and we
- // especially don't want to leave a file locked after we're done with it. Our
- // Close method is what releases the locks, so use a finalizer to report
- // missing Close calls on a best-effort basis.
- runtime.SetFinalizer(f, func(f *File) {
- panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", f.Name()))
- })
-
- return f, nil
-}
-
-// Open is like os.Open, but returns a read-locked file.
-func Open(name string) (*File, error) {
- return OpenFile(name, os.O_RDONLY, 0)
-}
-
-// Create is like os.Create, but returns a write-locked file.
-func Create(name string) (*File, error) {
- return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
-}
-
-// Edit creates the named file with mode 0666 (before umask),
-// but does not truncate existing contents.
-//
-// If Edit succeeds, methods on the returned File can be used for I/O.
-// The associated file descriptor has mode O_RDWR and the file is write-locked.
-func Edit(name string) (*File, error) {
- return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
-}
-
-// Close unlocks and closes the underlying file.
-//
-// Close may be called multiple times; all calls after the first will return a
-// non-nil error.
-func (f *File) Close() error {
- if f.closed {
- return &fs.PathError{
- Op: "close",
- Path: f.Name(),
- Err: fs.ErrClosed,
- }
- }
- f.closed = true
-
- err := closeFile(f.osFile.File)
- runtime.SetFinalizer(f, nil)
- return err
-}
-
-// Read opens the named file with a read-lock and returns its contents.
-func Read(name string) ([]byte, error) {
- f, err := Open(name)
- if err != nil {
- return nil, err
- }
- defer f.Close()
-
- return io.ReadAll(f)
-}
-
-// Write opens the named file (creating it with the given permissions if needed),
-// then write-locks it and overwrites it with the given content.
-func Write(name string, content io.Reader, perm fs.FileMode) (err error) {
- f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
- if err != nil {
- return err
- }
-
- _, err = io.Copy(f, content)
- if closeErr := f.Close(); err == nil {
- err = closeErr
- }
- return err
-}
-
-// Transform invokes t with the result of reading the named file, with its lock
-// still held.
-//
-// If t returns a nil error, Transform then writes the returned contents back to
-// the file, making a best effort to preserve existing contents on error.
-//
-// t must not modify the slice passed to it.
-func Transform(name string, t func([]byte) ([]byte, error)) (err error) {
- f, err := Edit(name)
- if err != nil {
- return err
- }
- defer f.Close()
-
- old, err := io.ReadAll(f)
- if err != nil {
- return err
- }
-
- new, err := t(old)
- if err != nil {
- return err
- }
-
- if len(new) > len(old) {
- // The overall file size is increasing, so write the tail first: if we're
- // about to run out of space on the disk, we would rather detect that
- // failure before we have overwritten the original contents.
- if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
- // Make a best effort to remove the incomplete tail.
- f.Truncate(int64(len(old)))
- return err
- }
- }
-
- // We're about to overwrite the old contents. In case of failure, make a best
- // effort to roll back before we close the file.
- defer func() {
- if err != nil {
- if _, err := f.WriteAt(old, 0); err == nil {
- f.Truncate(int64(len(old)))
- }
- }
- }()
-
- if len(new) >= len(old) {
- if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
- return err
- }
- } else {
- if _, err := f.WriteAt(new, 0); err != nil {
- return err
- }
- // The overall file size is decreasing, so shrink the file to its final size
- // after writing. We do this after writing (instead of before) so that if
- // the write fails, enough filesystem space will likely still be reserved
- // to contain the previous contents.
- if err := f.Truncate(int64(len(new))); err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/internal/lockedfile/lockedfile_filelock.go b/internal/lockedfile/lockedfile_filelock.go
deleted file mode 100644
index 7c71672..0000000
--- a/internal/lockedfile/lockedfile_filelock.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build !plan9
-// +build !plan9
-
-package lockedfile
-
-import (
- "io/fs"
- "os"
-
- "golang.org/x/tools/internal/lockedfile/internal/filelock"
-)
-
-func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
- // On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile
- // call instead of locking separately, but we have to support separate locking
- // calls for Linux and Windows anyway, so it's simpler to use that approach
- // consistently.
-
- f, err := os.OpenFile(name, flag&^os.O_TRUNC, perm)
- if err != nil {
- return nil, err
- }
-
- switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
- case os.O_WRONLY, os.O_RDWR:
- err = filelock.Lock(f)
- default:
- err = filelock.RLock(f)
- }
- if err != nil {
- f.Close()
- return nil, err
- }
-
- if flag&os.O_TRUNC == os.O_TRUNC {
- if err := f.Truncate(0); err != nil {
- // The documentation for os.O_TRUNC says “if possible, truncate file when
- // opened”, but doesn't define “possible” (golang.org/issue/28699).
- // We'll treat regular files (and symlinks to regular files) as “possible”
- // and ignore errors for the rest.
- if fi, statErr := f.Stat(); statErr != nil || fi.Mode().IsRegular() {
- filelock.Unlock(f)
- f.Close()
- return nil, err
- }
- }
- }
-
- return f, nil
-}
-
-func closeFile(f *os.File) error {
- // Since locking syscalls operate on file descriptors, we must unlock the file
- // while the descriptor is still valid — that is, before the file is closed —
- // and avoid unlocking files that are already closed.
- err := filelock.Unlock(f)
-
- if closeErr := f.Close(); err == nil {
- err = closeErr
- }
- return err
-}
diff --git a/internal/lockedfile/lockedfile_plan9.go b/internal/lockedfile/lockedfile_plan9.go
deleted file mode 100644
index 40871e6..0000000
--- a/internal/lockedfile/lockedfile_plan9.go
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build plan9
-// +build plan9
-
-package lockedfile
-
-import (
- "io/fs"
- "math/rand"
- "os"
- "strings"
- "time"
-)
-
-// Opening an exclusive-use file returns an error.
-// The expected error strings are:
-//
-// - "open/create -- file is locked" (cwfs, kfs)
-// - "exclusive lock" (fossil)
-// - "exclusive use file already open" (ramfs)
-var lockedErrStrings = [...]string{
- "file is locked",
- "exclusive lock",
- "exclusive use file already open",
-}
-
-// Even though plan9 doesn't support the Lock/RLock/Unlock functions to
-// manipulate already-open files, IsLocked is still meaningful: os.OpenFile
-// itself may return errors that indicate that a file with the ModeExclusive bit
-// set is already open.
-func isLocked(err error) bool {
- s := err.Error()
-
- for _, frag := range lockedErrStrings {
- if strings.Contains(s, frag) {
- return true
- }
- }
-
- return false
-}
-
-func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
- // Plan 9 uses a mode bit instead of explicit lock/unlock syscalls.
- //
- // Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open
- // for I/O by only one fid at a time across all clients of the server. If a
- // second open is attempted, it draws an error.”
- //
- // So we can try to open a locked file, but if it fails we're on our own to
- // figure out when it becomes available. We'll use exponential backoff with
- // some jitter and an arbitrary limit of 500ms.
-
- // If the file was unpacked or created by some other program, it might not
- // have the ModeExclusive bit set. Set it before we call OpenFile, so that we
- // can be confident that a successful OpenFile implies exclusive use.
- if fi, err := os.Stat(name); err == nil {
- if fi.Mode()&fs.ModeExclusive == 0 {
- if err := os.Chmod(name, fi.Mode()|fs.ModeExclusive); err != nil {
- return nil, err
- }
- }
- } else if !os.IsNotExist(err) {
- return nil, err
- }
-
- nextSleep := 1 * time.Millisecond
- const maxSleep = 500 * time.Millisecond
- for {
- f, err := os.OpenFile(name, flag, perm|fs.ModeExclusive)
- if err == nil {
- return f, nil
- }
-
- if !isLocked(err) {
- return nil, err
- }
-
- time.Sleep(nextSleep)
-
- nextSleep += nextSleep
- if nextSleep > maxSleep {
- nextSleep = maxSleep
- }
- // Apply 10% jitter to avoid synchronizing collisions.
- nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
- }
-}
-
-func closeFile(f *os.File) error {
- return f.Close()
-}
diff --git a/internal/lockedfile/lockedfile_test.go b/internal/lockedfile/lockedfile_test.go
deleted file mode 100644
index edf8851..0000000
--- a/internal/lockedfile/lockedfile_test.go
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows
-
-package lockedfile_test
-
-import (
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "testing"
- "time"
-
- "golang.org/x/tools/internal/lockedfile"
-)
-
-func mustTempDir(t *testing.T) (dir string, remove func()) {
- t.Helper()
-
- dir, err := os.MkdirTemp("", filepath.Base(t.Name()))
- if err != nil {
- t.Fatal(err)
- }
- return dir, func() { os.RemoveAll(dir) }
-}
-
-const (
- quiescent = 10 * time.Millisecond
- probablyStillBlocked = 10 * time.Second
-)
-
-func mustBlock(t *testing.T, desc string, f func()) (wait func(*testing.T)) {
- t.Helper()
-
- done := make(chan struct{})
- go func() {
- f()
- close(done)
- }()
-
- select {
- case <-done:
- t.Fatalf("%s unexpectedly did not block", desc)
- return nil
-
- case <-time.After(quiescent):
- return func(t *testing.T) {
- t.Helper()
- select {
- case <-time.After(probablyStillBlocked):
- t.Fatalf("%s is unexpectedly still blocked after %v", desc, probablyStillBlocked)
- case <-done:
- }
- }
- }
-}
-
-func TestMutexExcludes(t *testing.T) {
- t.Parallel()
-
- dir, remove := mustTempDir(t)
- defer remove()
-
- path := filepath.Join(dir, "lock")
-
- mu := lockedfile.MutexAt(path)
- t.Logf("mu := MutexAt(_)")
-
- unlock, err := mu.Lock()
- if err != nil {
- t.Fatalf("mu.Lock: %v", err)
- }
- t.Logf("unlock, _ := mu.Lock()")
-
- mu2 := lockedfile.MutexAt(mu.Path)
- t.Logf("mu2 := MutexAt(mu.Path)")
-
- wait := mustBlock(t, "mu2.Lock()", func() {
- unlock2, err := mu2.Lock()
- if err != nil {
- t.Errorf("mu2.Lock: %v", err)
- return
- }
- t.Logf("unlock2, _ := mu2.Lock()")
- t.Logf("unlock2()")
- unlock2()
- })
-
- t.Logf("unlock()")
- unlock()
- wait(t)
-}
-
-func TestReadWaitsForLock(t *testing.T) {
- t.Parallel()
-
- dir, remove := mustTempDir(t)
- defer remove()
-
- path := filepath.Join(dir, "timestamp.txt")
-
- f, err := lockedfile.Create(path)
- if err != nil {
- t.Fatalf("Create: %v", err)
- }
- defer f.Close()
-
- const (
- part1 = "part 1\n"
- part2 = "part 2\n"
- )
- _, err = f.WriteString(part1)
- if err != nil {
- t.Fatalf("WriteString: %v", err)
- }
- t.Logf("WriteString(%q) = <nil>", part1)
-
- wait := mustBlock(t, "Read", func() {
- b, err := lockedfile.Read(path)
- if err != nil {
- t.Errorf("Read: %v", err)
- return
- }
-
- const want = part1 + part2
- got := string(b)
- if got == want {
- t.Logf("Read(_) = %q", got)
- } else {
- t.Errorf("Read(_) = %q, _; want %q", got, want)
- }
- })
-
- _, err = f.WriteString(part2)
- if err != nil {
- t.Errorf("WriteString: %v", err)
- } else {
- t.Logf("WriteString(%q) = <nil>", part2)
- }
- f.Close()
-
- wait(t)
-}
-
-func TestCanLockExistingFile(t *testing.T) {
- t.Parallel()
-
- dir, remove := mustTempDir(t)
- defer remove()
- path := filepath.Join(dir, "existing.txt")
-
- if err := os.WriteFile(path, []byte("ok"), 0777); err != nil {
- t.Fatalf("os.WriteFile: %v", err)
- }
-
- f, err := lockedfile.Edit(path)
- if err != nil {
- t.Fatalf("first Edit: %v", err)
- }
-
- wait := mustBlock(t, "Edit", func() {
- other, err := lockedfile.Edit(path)
- if err != nil {
- t.Errorf("second Edit: %v", err)
- }
- other.Close()
- })
-
- f.Close()
- wait(t)
-}
-
-// TestSpuriousEDEADLK verifies that the spurious EDEADLK reported in
-// https://golang.org/issue/32817 no longer occurs.
-func TestSpuriousEDEADLK(t *testing.T) {
- // P.1 locks file A.
- // Q.3 locks file B.
- // Q.3 blocks on file A.
- // P.2 blocks on file B. (Spurious EDEADLK occurs here.)
- // P.1 unlocks file A.
- // Q.3 unblocks and locks file A.
- // Q.3 unlocks files A and B.
- // P.2 unblocks and locks file B.
- // P.2 unlocks file B.
-
- dirVar := t.Name() + "DIR"
-
- if dir := os.Getenv(dirVar); dir != "" {
- // Q.3 locks file B.
- b, err := lockedfile.Edit(filepath.Join(dir, "B"))
- if err != nil {
- t.Fatal(err)
- }
- defer b.Close()
-
- if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil {
- t.Fatal(err)
- }
-
- // Q.3 blocks on file A.
- a, err := lockedfile.Edit(filepath.Join(dir, "A"))
- // Q.3 unblocks and locks file A.
- if err != nil {
- t.Fatal(err)
- }
- defer a.Close()
-
- // Q.3 unlocks files A and B.
- return
- }
-
- dir, remove := mustTempDir(t)
- defer remove()
-
- // P.1 locks file A.
- a, err := lockedfile.Edit(filepath.Join(dir, "A"))
- if err != nil {
- t.Fatal(err)
- }
-
- cmd := exec.Command(os.Args[0], "-test.run="+t.Name())
- cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir))
-
- qDone := make(chan struct{})
- waitQ := mustBlock(t, "Edit A and B in subprocess", func() {
- out, err := cmd.CombinedOutput()
- if err != nil {
- t.Errorf("%v:\n%s", err, out)
- }
- close(qDone)
- })
-
- // Wait until process Q has either failed or locked file B.
- // Otherwise, P.2 might not block on file B as intended.
-locked:
- for {
- if _, err := os.Stat(filepath.Join(dir, "locked")); !os.IsNotExist(err) {
- break locked
- }
- select {
- case <-qDone:
- break locked
- case <-time.After(1 * time.Millisecond):
- }
- }
-
- waitP2 := mustBlock(t, "Edit B", func() {
- // P.2 blocks on file B. (Spurious EDEADLK occurs here.)
- b, err := lockedfile.Edit(filepath.Join(dir, "B"))
- // P.2 unblocks and locks file B.
- if err != nil {
- t.Error(err)
- return
- }
- // P.2 unlocks file B.
- b.Close()
- })
-
- // P.1 unlocks file A.
- a.Close()
-
- waitQ(t)
- waitP2(t)
-}
diff --git a/internal/lockedfile/mutex.go b/internal/lockedfile/mutex.go
deleted file mode 100644
index 180a36c..0000000
--- a/internal/lockedfile/mutex.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package lockedfile
-
-import (
- "fmt"
- "os"
- "sync"
-)
-
-// A Mutex provides mutual exclusion within and across processes by locking a
-// well-known file. Such a file generally guards some other part of the
-// filesystem: for example, a Mutex file in a directory might guard access to
-// the entire tree rooted in that directory.
-//
-// Mutex does not implement sync.Locker: unlike a sync.Mutex, a lockedfile.Mutex
-// can fail to lock (e.g. if there is a permission error in the filesystem).
-//
-// Like a sync.Mutex, a Mutex may be included as a field of a larger struct but
-// must not be copied after first use. The Path field must be set before first
-// use and must not be change thereafter.
-type Mutex struct {
- Path string // The path to the well-known lock file. Must be non-empty.
- mu sync.Mutex // A redundant mutex. The race detector doesn't know about file locking, so in tests we may need to lock something that it understands.
-}
-
-// MutexAt returns a new Mutex with Path set to the given non-empty path.
-func MutexAt(path string) *Mutex {
- if path == "" {
- panic("lockedfile.MutexAt: path must be non-empty")
- }
- return &Mutex{Path: path}
-}
-
-func (mu *Mutex) String() string {
- return fmt.Sprintf("lockedfile.Mutex(%s)", mu.Path)
-}
-
-// Lock attempts to lock the Mutex.
-//
-// If successful, Lock returns a non-nil unlock function: it is provided as a
-// return-value instead of a separate method to remind the caller to check the
-// accompanying error. (See https://golang.org/issue/20803.)
-func (mu *Mutex) Lock() (unlock func(), err error) {
- if mu.Path == "" {
- panic("lockedfile.Mutex: missing Path during Lock")
- }
-
- // We could use either O_RDWR or O_WRONLY here. If we choose O_RDWR and the
- // file at mu.Path is write-only, the call to OpenFile will fail with a
- // permission error. That's actually what we want: if we add an RLock method
- // in the future, it should call OpenFile with O_RDONLY and will require the
- // files must be readable, so we should not let the caller make any
- // assumptions about Mutex working with write-only files.
- f, err := OpenFile(mu.Path, os.O_RDWR|os.O_CREATE, 0666)
- if err != nil {
- return nil, err
- }
- mu.mu.Lock()
-
- return func() {
- mu.mu.Unlock()
- f.Close()
- }, nil
-}
diff --git a/internal/lockedfile/transform_test.go b/internal/lockedfile/transform_test.go
deleted file mode 100644
index 2c0cd53..0000000
--- a/internal/lockedfile/transform_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows
-
-package lockedfile_test
-
-import (
- "bytes"
- "encoding/binary"
- "math/rand"
- "path/filepath"
- "testing"
- "time"
-
- "golang.org/x/tools/internal/lockedfile"
-)
-
-func isPowerOf2(x int) bool {
- return x > 0 && x&(x-1) == 0
-}
-
-func roundDownToPowerOf2(x int) int {
- if x <= 0 {
- panic("nonpositive x")
- }
- bit := 1
- for x != bit {
- x = x &^ bit
- bit <<= 1
- }
- return x
-}
-
-func TestTransform(t *testing.T) {
- dir, remove := mustTempDir(t)
- defer remove()
- path := filepath.Join(dir, "blob.bin")
-
- const maxChunkWords = 8 << 10
- buf := make([]byte, 2*maxChunkWords*8)
- for i := uint64(0); i < 2*maxChunkWords; i++ {
- binary.LittleEndian.PutUint64(buf[i*8:], i)
- }
- if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil {
- t.Fatal(err)
- }
-
- var attempts int64 = 128
- if !testing.Short() {
- attempts *= 16
- }
- const parallel = 32
-
- var sem = make(chan bool, parallel)
-
- for n := attempts; n > 0; n-- {
- sem <- true
- go func() {
- defer func() { <-sem }()
-
- time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
- chunkWords := roundDownToPowerOf2(rand.Intn(maxChunkWords) + 1)
- offset := rand.Intn(chunkWords)
-
- err := lockedfile.Transform(path, func(data []byte) (chunk []byte, err error) {
- chunk = buf[offset*8 : (offset+chunkWords)*8]
-
- if len(data)&^7 != len(data) {
- t.Errorf("read %d bytes, but each write is an integer multiple of 8 bytes", len(data))
- return chunk, nil
- }
-
- words := len(data) / 8
- if !isPowerOf2(words) {
- t.Errorf("read %d 8-byte words, but each write is a power-of-2 number of words", words)
- return chunk, nil
- }
-
- u := binary.LittleEndian.Uint64(data)
- for i := 1; i < words; i++ {
- next := binary.LittleEndian.Uint64(data[i*8:])
- if next != u+1 {
- t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i)
- return chunk, nil
- }
- u = next
- }
-
- return chunk, nil
- })
-
- if err != nil {
- t.Errorf("unexpected error from Transform: %v", err)
- }
- }()
- }
-
- for n := parallel; n > 0; n-- {
- sem <- true
- }
-}
diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go
index 3c53fbc..ce7d435 100644
--- a/internal/typesinternal/types.go
+++ b/internal/typesinternal/types.go
@@ -11,8 +11,6 @@
"go/types"
"reflect"
"unsafe"
-
- "golang.org/x/tools/go/types/objectpath"
)
func SetUsesCgo(conf *types.Config) bool {
@@ -52,10 +50,3 @@
}
var SetGoVersion = func(conf *types.Config, version string) bool { return false }
-
-// NewObjectpathEncoder returns a function closure equivalent to
-// objectpath.For but amortized for multiple (sequential) calls.
-// It is a temporary workaround, pending the approval of proposal 58668.
-//
-//go:linkname NewObjectpathFunc golang.org/x/tools/go/types/objectpath.newEncoderFor
-func NewObjectpathFunc() func(types.Object) (objectpath.Path, error)
diff --git a/present/doc.go b/present/doc.go
index 71f758f..2c88fb9 100644
--- a/present/doc.go
+++ b/present/doc.go
@@ -200,8 +200,11 @@
a single marker character becomes a space and a doubled single
marker quotes the marker character.
-Links can be included in any text with the form [[url][label]], or
-[[url]] to use the URL itself as the label.
+Links can be included in any text with either explicit labels
+or the URL itself as the label. For example:
+
+ [[url][label]]
+ [[url]]
# Command Invocations