aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/regtest/misc
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/regtest/misc')
-rw-r--r--gopls/internal/regtest/misc/call_hierarchy_test.go10
-rw-r--r--gopls/internal/regtest/misc/configuration_test.go132
-rw-r--r--gopls/internal/regtest/misc/debugserver_test.go6
-rw-r--r--gopls/internal/regtest/misc/definition_test.go301
-rw-r--r--gopls/internal/regtest/misc/embed_test.go13
-rw-r--r--gopls/internal/regtest/misc/extract_test.go65
-rw-r--r--gopls/internal/regtest/misc/failures_test.go46
-rw-r--r--gopls/internal/regtest/misc/fix_test.go24
-rw-r--r--gopls/internal/regtest/misc/formatting_test.go45
-rw-r--r--gopls/internal/regtest/misc/generate_test.go19
-rw-r--r--gopls/internal/regtest/misc/highlight_test.go36
-rw-r--r--gopls/internal/regtest/misc/hover_test.go272
-rw-r--r--gopls/internal/regtest/misc/import_test.go12
-rw-r--r--gopls/internal/regtest/misc/imports_test.go78
-rw-r--r--gopls/internal/regtest/misc/leak_test.go89
-rw-r--r--gopls/internal/regtest/misc/link_test.go27
-rw-r--r--gopls/internal/regtest/misc/misc_test.go4
-rw-r--r--gopls/internal/regtest/misc/multiple_adhoc_test.go8
-rw-r--r--gopls/internal/regtest/misc/references_test.go340
-rw-r--r--gopls/internal/regtest/misc/rename_test.go885
-rw-r--r--gopls/internal/regtest/misc/semantictokens_test.go172
-rw-r--r--gopls/internal/regtest/misc/settings_test.go8
-rw-r--r--gopls/internal/regtest/misc/shared_test.go58
-rw-r--r--gopls/internal/regtest/misc/signature_help_test.go69
-rw-r--r--gopls/internal/regtest/misc/staticcheck_test.go110
-rw-r--r--gopls/internal/regtest/misc/vendor_test.go26
-rw-r--r--gopls/internal/regtest/misc/vuln_test.go948
-rw-r--r--gopls/internal/regtest/misc/workspace_symbol_test.go49
28 files changed, 3545 insertions, 307 deletions
diff --git a/gopls/internal/regtest/misc/call_hierarchy_test.go b/gopls/internal/regtest/misc/call_hierarchy_test.go
index 9d98896ce..f0f5d4a41 100644
--- a/gopls/internal/regtest/misc/call_hierarchy_test.go
+++ b/gopls/internal/regtest/misc/call_hierarchy_test.go
@@ -6,8 +6,8 @@ package misc
import (
"testing"
- "golang.org/x/tools/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
// Test for golang/go#49125
@@ -23,11 +23,11 @@ package pkg
// TODO(rfindley): this could probably just be a marker test.
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("p.go")
- pos := env.RegexpSearch("p.go", "pkg")
+ loc := env.RegexpSearch("p.go", "pkg")
var params protocol.CallHierarchyPrepareParams
- params.TextDocument.URI = env.Sandbox.Workdir.URI("p.go")
- params.Position = pos.ToProtocolPosition()
+ params.TextDocument.URI = loc.URI
+ params.Position = loc.Range.Start
// Check that this doesn't panic.
env.Editor.Server.PrepareCallHierarchy(env.Ctx, &params)
diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go
index 17116adaa..6cbfe373e 100644
--- a/gopls/internal/regtest/misc/configuration_test.go
+++ b/gopls/internal/regtest/misc/configuration_test.go
@@ -7,17 +7,18 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
- "golang.org/x/tools/internal/lsp/fake"
"golang.org/x/tools/internal/testenv"
)
// Test that enabling and disabling produces the expected results of showing
// and hiding staticcheck analysis results.
func TestChangeConfiguration(t *testing.T) {
- // Staticcheck only supports Go versions > 1.14.
- testenv.NeedsGo1Point(t, 15)
+ // Staticcheck only supports Go versions >= 1.19.
+ // Note: keep this in sync with TestStaticcheckWarning. Below this version we
+ // should get an error when setting staticcheck configuration.
+ testenv.NeedsGo1Point(t, 19)
const files = `
-- go.mod --
@@ -34,16 +35,125 @@ var FooErr = errors.New("foo")
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
+ env.AfterChange(
+ NoDiagnostics(ForFile("a/a.go")),
+ )
+ cfg := env.Editor.Config()
+ cfg.Settings = map[string]interface{}{
+ "staticcheck": true,
+ }
+ // TODO(rfindley): support waiting on diagnostics following a configuration
+ // change.
+ env.ChangeConfiguration(cfg)
env.Await(
- env.DoneWithOpen(),
- NoDiagnostics("a/a.go"),
+ Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")),
+ )
+ })
+}
+
+// TestMajorOptionsChange is like TestChangeConfiguration, but modifies an
+// an open buffer before making a major (but inconsequential) change that
+// causes gopls to recreate the view.
+//
+// Gopls should not get confused about buffer content when recreating the view.
+func TestMajorOptionsChange(t *testing.T) {
+ t.Skip("broken due to golang/go#57934")
+
+ testenv.NeedsGo1Point(t, 17)
+
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- a/a.go --
+package a
+
+import "errors"
+
+var ErrFoo = errors.New("foo")
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+ // Introduce a staticcheck diagnostic. It should be detected when we enable
+ // staticcheck later.
+ env.RegexpReplace("a/a.go", "ErrFoo", "FooErr")
+ env.AfterChange(
+ NoDiagnostics(ForFile("a/a.go")),
)
- cfg := &fake.EditorConfig{}
- *cfg = env.Editor.Config
- cfg.EnableStaticcheck = true
- env.ChangeConfiguration(t, cfg)
+ cfg := env.Editor.Config()
+ // Any change to environment recreates the view, but this should not cause
+ // gopls to get confused about the content of a/a.go: we should get the
+ // staticcheck diagnostic below.
+ cfg.Env = map[string]string{
+ "AN_ARBITRARY_VAR": "FOO",
+ }
+ cfg.Settings = map[string]interface{}{
+ "staticcheck": true,
+ }
+ // TODO(rfindley): support waiting on diagnostics following a configuration
+ // change.
+ env.ChangeConfiguration(cfg)
env.Await(
- DiagnosticAt("a/a.go", 5, 4),
+ Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")),
+ )
+ })
+}
+
+func TestStaticcheckWarning(t *testing.T) {
+ // Note: keep this in sync with TestChangeConfiguration.
+ testenv.SkipAfterGo1Point(t, 16)
+
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- a/a.go --
+package a
+
+import "errors"
+
+// FooErr should be called ErrFoo (ST1012)
+var FooErr = errors.New("foo")
+`
+
+ WithOptions(
+ Settings{"staticcheck": true},
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ ShownMessage("staticcheck is not supported"),
+ )
+ })
+}
+
+func TestGofumptWarning(t *testing.T) {
+ testenv.SkipAfterGo1Point(t, 17)
+
+ WithOptions(
+ Settings{"gofumpt": true},
+ ).Run(t, "", func(t *testing.T, env *Env) {
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ ShownMessage("gofumpt is not supported"),
+ )
+ })
+}
+
+func TestDeprecatedSettings(t *testing.T) {
+ WithOptions(
+ Settings{
+ "experimentalUseInvalidMetadata": true,
+ "experimentalWatchedFileDelay": "1s",
+ "experimentalWorkspaceModule": true,
+ },
+ ).Run(t, "", func(t *testing.T, env *Env) {
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ ShownMessage("experimentalWorkspaceModule"),
+ ShownMessage("experimentalUseInvalidMetadata"),
+ ShownMessage("experimentalWatchedFileDelay"),
)
})
}
diff --git a/gopls/internal/regtest/misc/debugserver_test.go b/gopls/internal/regtest/misc/debugserver_test.go
index c0df87070..519f79447 100644
--- a/gopls/internal/regtest/misc/debugserver_test.go
+++ b/gopls/internal/regtest/misc/debugserver_test.go
@@ -8,10 +8,10 @@ import (
"net/http"
"testing"
- "golang.org/x/tools/internal/lsp/command"
- "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestStartDebugging(t *testing.T) {
diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go
index 2f5a54820..c2dd67fc3 100644
--- a/gopls/internal/regtest/misc/definition_test.go
+++ b/gopls/internal/regtest/misc/definition_test.go
@@ -5,16 +5,15 @@
package misc
import (
+ "os"
"path"
+ "path/filepath"
"strings"
"testing"
- "golang.org/x/tools/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
- "golang.org/x/tools/internal/testenv"
-
- "golang.org/x/tools/internal/lsp/fake"
- "golang.org/x/tools/internal/lsp/tests"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
)
const internalDefinition = `
@@ -39,12 +38,150 @@ const message = "Hello World."
func TestGoToInternalDefinition(t *testing.T) {
Run(t, internalDefinition, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- name, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "message"))
+ loc := env.GoToDefinition(env.RegexpSearch("main.go", "message"))
+ name := env.Sandbox.Workdir.URIToPath(loc.URI)
if want := "const.go"; name != want {
t.Errorf("GoToDefinition: got file %q, want %q", name, want)
}
- if want := env.RegexpSearch("const.go", "message"); pos != want {
- t.Errorf("GoToDefinition: got position %v, want %v", pos, want)
+ if want := env.RegexpSearch("const.go", "message"); loc != want {
+ t.Errorf("GoToDefinition: got location %v, want %v", loc, want)
+ }
+ })
+}
+
+const linknameDefinition = `
+-- go.mod --
+module mod.com
+
+-- upper/upper.go --
+package upper
+
+import (
+ _ "unsafe"
+
+ _ "mod.com/middle"
+)
+
+//go:linkname foo mod.com/lower.bar
+func foo() string
+
+-- middle/middle.go --
+package middle
+
+import (
+ _ "mod.com/lower"
+)
+
+-- lower/lower.s --
+
+-- lower/lower.go --
+package lower
+
+func bar() string {
+ return "bar as foo"
+}`
+
+func TestGoToLinknameDefinition(t *testing.T) {
+ Run(t, linknameDefinition, func(t *testing.T, env *Env) {
+ env.OpenFile("upper/upper.go")
+
+ // Jump from directives 2nd arg.
+ start := env.RegexpSearch("upper/upper.go", `lower.bar`)
+ loc := env.GoToDefinition(start)
+ name := env.Sandbox.Workdir.URIToPath(loc.URI)
+ if want := "lower/lower.go"; name != want {
+ t.Errorf("GoToDefinition: got file %q, want %q", name, want)
+ }
+ if want := env.RegexpSearch("lower/lower.go", `bar`); loc != want {
+ t.Errorf("GoToDefinition: got position %v, want %v", loc, want)
+ }
+ })
+}
+
+const linknameDefinitionReverse = `
+-- go.mod --
+module mod.com
+
+-- upper/upper.s --
+
+-- upper/upper.go --
+package upper
+
+import (
+ _ "mod.com/middle"
+)
+
+func foo() string
+
+-- middle/middle.go --
+package middle
+
+import (
+ _ "mod.com/lower"
+)
+
+-- lower/lower.go --
+package lower
+
+import _ "unsafe"
+
+//go:linkname bar mod.com/upper.foo
+func bar() string {
+ return "bar as foo"
+}`
+
+func TestGoToLinknameDefinitionInReverseDep(t *testing.T) {
+ Run(t, linknameDefinitionReverse, func(t *testing.T, env *Env) {
+ env.OpenFile("lower/lower.go")
+
+ // Jump from directives 2nd arg.
+ start := env.RegexpSearch("lower/lower.go", `upper.foo`)
+ loc := env.GoToDefinition(start)
+ name := env.Sandbox.Workdir.URIToPath(loc.URI)
+ if want := "upper/upper.go"; name != want {
+ t.Errorf("GoToDefinition: got file %q, want %q", name, want)
+ }
+ if want := env.RegexpSearch("upper/upper.go", `foo`); loc != want {
+ t.Errorf("GoToDefinition: got position %v, want %v", loc, want)
+ }
+ })
+}
+
+// The linkname directive connects two packages not related in the import graph.
+const linknameDefinitionDisconnected = `
+-- go.mod --
+module mod.com
+
+-- a/a.go --
+package a
+
+import (
+ _ "unsafe"
+)
+
+//go:linkname foo mod.com/b.bar
+func foo() string
+
+-- b/b.go --
+package b
+
+func bar() string {
+ return "bar as foo"
+}`
+
+func TestGoToLinknameDefinitionDisconnected(t *testing.T) {
+ Run(t, linknameDefinitionDisconnected, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+
+ // Jump from directives 2nd arg.
+ start := env.RegexpSearch("a/a.go", `b.bar`)
+ loc := env.GoToDefinition(start)
+ name := env.Sandbox.Workdir.URIToPath(loc.URI)
+ if want := "b/b.go"; name != want {
+ t.Errorf("GoToDefinition: got file %q, want %q", name, want)
+ }
+ if want := env.RegexpSearch("b/b.go", `bar`); loc != want {
+ t.Errorf("GoToDefinition: got position %v, want %v", loc, want)
}
})
}
@@ -66,19 +203,21 @@ func main() {
func TestGoToStdlibDefinition_Issue37045(t *testing.T) {
Run(t, stdlibDefinition, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- name, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`))
+ loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`))
+ name := env.Sandbox.Workdir.URIToPath(loc.URI)
if got, want := path.Base(name), "print.go"; got != want {
t.Errorf("GoToDefinition: got file %q, want %q", name, want)
}
// Test that we can jump to definition from outside our workspace.
// See golang.org/issues/37045.
- newName, newPos := env.GoToDefinition(name, pos)
+ newLoc := env.GoToDefinition(loc)
+ newName := env.Sandbox.Workdir.URIToPath(newLoc.URI)
if newName != name {
t.Errorf("GoToDefinition is not idempotent: got %q, want %q", newName, name)
}
- if newPos != pos {
- t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newPos, pos)
+ if newLoc != loc {
+ t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newLoc, loc)
}
})
}
@@ -86,24 +225,24 @@ func TestGoToStdlibDefinition_Issue37045(t *testing.T) {
func TestUnexportedStdlib_Issue40809(t *testing.T) {
Run(t, stdlibDefinition, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- name, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`))
- env.OpenFile(name)
+ loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`))
+ name := env.Sandbox.Workdir.URIToPath(loc.URI)
- pos := env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`)
+ loc = env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`)
// Check that we can find references on a reference
- refs := env.References(name, pos)
+ refs := env.References(loc)
if len(refs) < 5 {
t.Errorf("expected 5+ references to newPrinter, found: %#v", refs)
}
- name, pos = env.GoToDefinition(name, pos)
- content, _ := env.Hover(name, pos)
+ loc = env.GoToDefinition(loc)
+ content, _ := env.Hover(loc)
if !strings.Contains(content.Value, "newPrinter") {
t.Fatal("definition of newPrinter went to the incorrect place")
}
// And on the definition too.
- refs = env.References(name, pos)
+ refs = env.References(loc)
if len(refs) < 5 {
t.Errorf("expected 5+ references to newPrinter, found: %#v", refs)
}
@@ -127,13 +266,13 @@ func main() {
}`
Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error"))
+ content, _ := env.Hover(env.RegexpSearch("main.go", "Error"))
if content == nil {
t.Fatalf("nil hover content for Error")
}
want := "```go\nfunc (error).Error() string\n```"
if content.Value != want {
- t.Fatalf("hover failed:\n%s", tests.Diff(t, want, content.Value))
+ t.Fatalf("hover failed:\n%s", compare.Text(want, content.Value))
}
})
}
@@ -153,24 +292,19 @@ func main() {}
`
for _, tt := range []struct {
wantLinks int
- wantDef bool
importShortcut string
}{
- {1, false, "Link"},
- {0, true, "Definition"},
- {1, true, "Both"},
+ {1, "Link"},
+ {0, "Definition"},
+ {1, "Both"},
} {
t.Run(tt.importShortcut, func(t *testing.T) {
WithOptions(
- EditorConfig{
- ImportShortcut: tt.importShortcut,
- },
+ Settings{"importShortcut": tt.importShortcut},
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"fmt"`))
- if !tt.wantDef && (file != "" || pos != (fake.Pos{})) {
- t.Fatalf("expected no definition, got one: %s:%v", file, pos)
- } else if tt.wantDef && file == "" && pos == (fake.Pos{}) {
+ loc := env.GoToDefinition(env.RegexpSearch("main.go", `"fmt"`))
+ if loc == (protocol.Location{}) {
t.Fatalf("expected definition, got none")
}
links := env.DocumentLink("main.go")
@@ -217,7 +351,7 @@ func main() {}
Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- _, pos, err := env.Editor.GoToTypeDefinition(env.Ctx, "main.go", env.RegexpSearch("main.go", tt.re))
+ loc, err := env.Editor.GoToTypeDefinition(env.Ctx, env.RegexpSearch("main.go", tt.re))
if tt.wantError {
if err == nil {
t.Fatal("expected error, got nil")
@@ -228,9 +362,9 @@ func main() {}
t.Fatalf("expected nil error, got %s", err)
}
- typePos := env.RegexpSearch("main.go", tt.wantTypeRe)
- if pos != typePos {
- t.Errorf("invalid pos: want %+v, got %+v", typePos, pos)
+ typeLoc := env.RegexpSearch("main.go", tt.wantTypeRe)
+ if loc != typeLoc {
+ t.Errorf("invalid pos: want %+v, got %+v", typeLoc, loc)
}
})
})
@@ -239,8 +373,6 @@ func main() {}
// Test for golang/go#47825.
func TestImportTestVariant(t *testing.T) {
- testenv.NeedsGo1Point(t, 13)
-
const mod = `
-- go.mod --
module mod.com
@@ -275,7 +407,7 @@ package client
`
Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("client/client_role_test.go")
- env.GoToDefinition("client/client_role_test.go", env.RegexpSearch("client/client_role_test.go", "RoleSetup"))
+ env.GoToDefinition(env.RegexpSearch("client/client_role_test.go", "RoleSetup"))
})
}
@@ -289,3 +421,92 @@ func TestGoToCrashingDefinition_Issue49223(t *testing.T) {
env.Editor.Server.Definition(env.Ctx, params)
})
}
+
+// TestVendoringInvalidatesMetadata ensures that gopls uses the
+// correct metadata even after an external 'go mod vendor' command
+// causes packages to move; see issue #55995.
+// See also TestImplementationsInVendor, which tests the same fix.
+func TestVendoringInvalidatesMetadata(t *testing.T) {
+ t.Skip("golang/go#56169: file watching does not capture vendor dirs")
+
+ const proxy = `
+-- other.com/b@v1.0.0/go.mod --
+module other.com/b
+go 1.14
+
+-- other.com/b@v1.0.0/b.go --
+package b
+const K = 0
+`
+ const src = `
+-- go.mod --
+module example.com/a
+go 1.14
+require other.com/b v1.0.0
+
+-- go.sum --
+other.com/b v1.0.0 h1:1wb3PMGdet5ojzrKl+0iNksRLnOM9Jw+7amBNqmYwqk=
+other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g=
+
+-- a.go --
+package a
+import "other.com/b"
+const _ = b.K
+
+`
+ WithOptions(
+ ProxyFiles(proxy),
+ Modes(Default), // fails in 'experimental' mode
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ // Enable to debug go.sum mismatch, which may appear as
+ // "module lookup disabled by GOPROXY=off", confusingly.
+ if false {
+ env.DumpGoSum(".")
+ }
+
+ env.OpenFile("a.go")
+ refLoc := env.RegexpSearch("a.go", "K") // find "b.K" reference
+
+ // Initially, b.K is defined in the module cache.
+ gotLoc := env.GoToDefinition(refLoc)
+ gotFile := env.Sandbox.Workdir.URIToPath(gotLoc.URI)
+ wantCache := filepath.ToSlash(env.Sandbox.GOPATH()) + "/pkg/mod/other.com/b@v1.0.0/b.go"
+ if gotFile != wantCache {
+ t.Errorf("GoToDefinition, before: got file %q, want %q", gotFile, wantCache)
+ }
+
+ // Run 'go mod vendor' outside the editor.
+ if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, true); err != nil {
+ t.Fatalf("go mod vendor: %v", err)
+ }
+
+ // Synchronize changes to watched files.
+ env.Await(env.DoneWithChangeWatchedFiles())
+
+ // Now, b.K is defined in the vendor tree.
+ gotLoc = env.GoToDefinition(refLoc)
+ wantVendor := "vendor/other.com/b/b.go"
+ if gotFile != wantVendor {
+ t.Errorf("GoToDefinition, after go mod vendor: got file %q, want %q", gotFile, wantVendor)
+ }
+
+ // Delete the vendor tree.
+ if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil {
+ t.Fatal(err)
+ }
+ // Notify the server of the deletion.
+ if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil {
+ t.Fatal(err)
+ }
+
+ // Synchronize again.
+ env.Await(env.DoneWithChangeWatchedFiles())
+
+ // b.K is once again defined in the module cache.
+ gotLoc = env.GoToDefinition(gotLoc)
+ gotFile = env.Sandbox.Workdir.URIToPath(gotLoc.URI)
+ if gotFile != wantCache {
+ t.Errorf("GoToDefinition, after rm -rf vendor: got file %q, want %q", gotFile, wantCache)
+ }
+ })
+}
diff --git a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go
index 2e66d7866..021fbfcc0 100644
--- a/gopls/internal/regtest/misc/embed_test.go
+++ b/gopls/internal/regtest/misc/embed_test.go
@@ -6,12 +6,10 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
- "golang.org/x/tools/internal/testenv"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestMissingPatternDiagnostic(t *testing.T) {
- testenv.NeedsGo1Point(t, 16)
const files = `
-- go.mod --
module example.com
@@ -30,8 +28,13 @@ var foo string
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("x.go")
- env.Await(env.DiagnosticAtRegexpWithMessage("x.go", `NONEXISTENT`, "no matching files found"))
+ env.AfterChange(
+ Diagnostics(
+ env.AtRegexp("x.go", `NONEXISTENT`),
+ WithMessage("no matching files found"),
+ ),
+ )
env.RegexpReplace("x.go", `NONEXISTENT`, "x.go")
- env.Await(EmptyDiagnostics("x.go"))
+ env.AfterChange(NoDiagnostics(ForFile("x.go")))
})
}
diff --git a/gopls/internal/regtest/misc/extract_test.go b/gopls/internal/regtest/misc/extract_test.go
new file mode 100644
index 000000000..23efffbb7
--- /dev/null
+++ b/gopls/internal/regtest/misc/extract_test.go
@@ -0,0 +1,65 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+package misc
+
+import (
+ "testing"
+
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+)
+
+func TestExtractFunction(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func Foo() int {
+ a := 5
+ return a
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ loc := env.RegexpSearch("main.go", `a := 5\n.*return a`)
+ actions, err := env.Editor.CodeAction(env.Ctx, loc, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Find the extract function code action.
+ var extractFunc *protocol.CodeAction
+ for _, action := range actions {
+ if action.Kind == protocol.RefactorExtract && action.Title == "Extract function" {
+ extractFunc = &action
+ break
+ }
+ }
+ if extractFunc == nil {
+ t.Fatal("could not find extract function action")
+ }
+
+ env.ApplyCodeAction(*extractFunc)
+ want := `package main
+
+func Foo() int {
+ return newFunction()
+}
+
+func newFunction() int {
+ a := 5
+ return a
+}
+`
+ if got := env.BufferText("main.go"); got != want {
+ t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got))
+ }
+ })
+}
diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go
index 23fccfd62..42aa3721a 100644
--- a/gopls/internal/regtest/misc/failures_test.go
+++ b/gopls/internal/regtest/misc/failures_test.go
@@ -7,12 +7,15 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
)
-// This test passes (TestHoverOnError in definition_test.go) without
-// the //line directive
+// This is a slight variant of TestHoverOnError in definition_test.go
+// that includes a line directive, which makes no difference since
+// gopls ignores line directives.
func TestHoverFailure(t *testing.T) {
+ t.Skip("line directives //line ")
const mod = `
-- go.mod --
module mod.com
@@ -29,19 +32,27 @@ func main() {
var err error
err.Error()
}`
- WithOptions(SkipLogs()).Run(t, mod, func(t *testing.T, env *Env) {
+ Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error"))
- // without the //line comment content would be non-nil
- if content != nil {
- t.Fatalf("expected nil hover content for Error")
+ content, _ := env.Hover(env.RegexpSearch("main.go", "Error"))
+ if content == nil {
+ t.Fatalf("Hover('Error') returned nil")
+ }
+ want := "```go\nfunc (error).Error() string\n```"
+ if content.Value != want {
+ t.Fatalf("wrong Hover('Error') content:\n%s", compare.Text(want, content.Value))
}
})
}
-// badPackageDup contains a duplicate definition of the 'a' const.
-// this is from diagnostics_test.go,
-const badPackageDup = `
+// This test demonstrates a case where gopls is 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.
+ const badPackageDup = `
-- go.mod --
module mod.com
@@ -56,15 +67,18 @@ package consts
const a = 2
`
-func TestFailingDiagnosticClearingOnEdit(t *testing.T) {
Run(t, badPackageDup, func(t *testing.T, env *Env) {
env.OpenFile("b.go")
- // no diagnostics for any files, but there should be
- env.Await(NoDiagnostics("a.go"), NoDiagnostics("b.go"))
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("b.go", `a = 2`), WithMessage("a redeclared")),
+ Diagnostics(env.AtRegexp("a.go", `a = 1`), WithMessage("other declaration")),
+ )
// Fix the error by editing the const name in b.go to `b`.
env.RegexpReplace("b.go", "(a) = 2", "b")
-
- // The diagnostics that weren't sent above should now be cleared.
+ env.AfterChange(
+ NoDiagnostics(ForFile("a.go")),
+ NoDiagnostics(ForFile("b.go")),
+ )
})
}
diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go
index 8318ae557..7a5e530e3 100644
--- a/gopls/internal/regtest/misc/fix_test.go
+++ b/gopls/internal/regtest/misc/fix_test.go
@@ -7,10 +7,10 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/lsp/tests"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
)
// A basic test for fillstruct, now that it uses a command.
@@ -34,11 +34,7 @@ func Foo() {
`
Run(t, basic, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- pos := env.RegexpSearch("main.go", "Info{}").ToProtocolPosition()
- if err := env.Editor.RefactorRewrite(env.Ctx, "main.go", &protocol.Range{
- Start: pos,
- End: pos,
- }); err != nil {
+ if err := env.Editor.RefactorRewrite(env.Ctx, env.RegexpSearch("main.go", "Info{}")); err != nil {
t.Fatal(err)
}
want := `package main
@@ -55,8 +51,8 @@ func Foo() {
}
}
`
- if got := env.Editor.BufferText("main.go"); got != want {
- t.Fatalf("TestFillStruct failed:\n%s", tests.Diff(t, want, got))
+ if got := env.BufferText("main.go"); got != want {
+ t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got))
}
})
}
@@ -77,11 +73,11 @@ func Foo() error {
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
var d protocol.PublishDiagnosticsParams
- env.Await(OnceMet(
+ env.AfterChange(
// The error message here changed in 1.18; "return values" covers both forms.
- env.DiagnosticAtRegexpWithMessage("main.go", `return`, "return values"),
+ Diagnostics(env.AtRegexp("main.go", `return`), WithMessage("return values")),
ReadDiagnostics("main.go", &d),
- ))
+ )
codeActions := env.CodeAction("main.go", d.Diagnostics)
if len(codeActions) != 2 {
t.Fatalf("expected 2 code actions, got %v", len(codeActions))
@@ -102,6 +98,6 @@ func Foo() error {
t.Fatalf("expected fixall code action, got none")
}
env.ApplyQuickFixes("main.go", d.Diagnostics)
- env.Await(EmptyDiagnostics("main.go"))
+ env.AfterChange(NoDiagnostics(ForFile("main.go")))
})
}
diff --git a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go
index 75d8f6224..ee8098cc9 100644
--- a/gopls/internal/regtest/misc/formatting_test.go
+++ b/gopls/internal/regtest/misc/formatting_test.go
@@ -8,9 +8,9 @@ import (
"strings"
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
-
- "golang.org/x/tools/internal/lsp/tests"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
+ "golang.org/x/tools/internal/testenv"
)
const unformattedProgram = `
@@ -34,10 +34,10 @@ func TestFormatting(t *testing.T) {
Run(t, unformattedProgram, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.FormatBuffer("main.go")
- got := env.Editor.BufferText("main.go")
+ got := env.BufferText("main.go")
want := env.ReadWorkspaceFile("main.go.golden")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
+ t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got))
}
})
}
@@ -56,10 +56,10 @@ func f() {}
Run(t, onelineProgram, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
env.FormatBuffer("a.go")
- got := env.Editor.BufferText("a.go")
+ got := env.BufferText("a.go")
want := env.ReadWorkspaceFile("a.go.formatted")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
+ t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got))
}
})
}
@@ -80,10 +80,10 @@ func f() { fmt.Println() }
Run(t, onelineProgramA, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
env.OrganizeImports("a.go")
- got := env.Editor.BufferText("a.go")
+ got := env.BufferText("a.go")
want := env.ReadWorkspaceFile("a.go.imported")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
+ t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got))
}
})
}
@@ -101,10 +101,10 @@ func f() {}
Run(t, onelineProgramB, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
env.OrganizeImports("a.go")
- got := env.Editor.BufferText("a.go")
+ got := env.BufferText("a.go")
want := env.ReadWorkspaceFile("a.go.imported")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
+ t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got))
}
})
}
@@ -147,10 +147,10 @@ func TestOrganizeImports(t *testing.T) {
Run(t, disorganizedProgram, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.OrganizeImports("main.go")
- got := env.Editor.BufferText("main.go")
+ got := env.BufferText("main.go")
want := env.ReadWorkspaceFile("main.go.organized")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
+ t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got))
}
})
}
@@ -159,10 +159,10 @@ func TestFormattingOnSave(t *testing.T) {
Run(t, disorganizedProgram, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.SaveBuffer("main.go")
- got := env.Editor.BufferText("main.go")
+ got := env.BufferText("main.go")
want := env.ReadWorkspaceFile("main.go.formatted")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
+ t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got))
}
})
}
@@ -259,10 +259,10 @@ func main() {
env.CreateBuffer("main.go", crlf)
env.Await(env.DoneWithOpen())
env.OrganizeImports("main.go")
- got := env.Editor.BufferText("main.go")
+ got := env.BufferText("main.go")
got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity
if tt.want != got {
- t.Errorf("unexpected content after save:\n%s", tests.Diff(t, tt.want, got))
+ t.Errorf("unexpected content after save:\n%s", compare.Text(tt.want, got))
}
})
})
@@ -303,6 +303,7 @@ func main() {
}
func TestGofumptFormatting(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
// Exercise some gofumpt formatting rules:
// - No empty lines following an assignment operator
@@ -352,18 +353,16 @@ const Bar = 42
`
WithOptions(
- EditorConfig{
- Settings: map[string]interface{}{
- "gofumpt": true,
- },
+ Settings{
+ "gofumpt": true,
},
).Run(t, input, func(t *testing.T, env *Env) {
env.OpenFile("foo.go")
env.FormatBuffer("foo.go")
- got := env.Editor.BufferText("foo.go")
+ got := env.BufferText("foo.go")
want := env.ReadWorkspaceFile("foo.go.formatted")
if got != want {
- t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
+ t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got))
}
})
}
diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go
index 1dc22d737..547755fd2 100644
--- a/gopls/internal/regtest/misc/generate_test.go
+++ b/gopls/internal/regtest/misc/generate_test.go
@@ -12,12 +12,10 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestGenerateProgress(t *testing.T) {
- t.Skipf("skipping flaky test: https://golang.org/issue/49901")
-
const generatedWorkspace = `
-- go.mod --
module fake.test
@@ -40,12 +38,12 @@ func main() {
-- lib1/lib.go --
package lib1
-//go:generate go run ../generate.go lib1
+//` + `go:generate go run ../generate.go lib1
-- lib2/lib.go --
package lib2
-//go:generate go run ../generate.go lib2
+//` + `go:generate go run ../generate.go lib2
-- main.go --
package main
@@ -61,15 +59,14 @@ func main() {
`
Run(t, generatedWorkspace, func(t *testing.T, env *Env) {
- env.Await(
- env.DiagnosticAtRegexp("main.go", "lib1.(Answer)"),
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ Diagnostics(env.AtRegexp("main.go", "lib1.(Answer)")),
)
env.RunGenerate("./lib1")
env.RunGenerate("./lib2")
- env.Await(
- OnceMet(
- env.DoneWithChangeWatchedFiles(),
- EmptyDiagnostics("main.go")),
+ env.AfterChange(
+ NoDiagnostics(ForFile("main.go")),
)
})
}
diff --git a/gopls/internal/regtest/misc/highlight_test.go b/gopls/internal/regtest/misc/highlight_test.go
index affbffd66..8835d608e 100644
--- a/gopls/internal/regtest/misc/highlight_test.go
+++ b/gopls/internal/regtest/misc/highlight_test.go
@@ -8,9 +8,8 @@ import (
"sort"
"testing"
- "golang.org/x/tools/internal/lsp/fake"
- "golang.org/x/tools/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestWorkspacePackageHighlight(t *testing.T) {
@@ -31,9 +30,9 @@ func main() {
Run(t, mod, func(t *testing.T, env *Env) {
const file = "main.go"
env.OpenFile(file)
- _, pos := env.GoToDefinition(file, env.RegexpSearch(file, `var (A) string`))
+ loc := env.GoToDefinition(env.RegexpSearch(file, `var (A) string`))
- checkHighlights(env, file, pos, 3)
+ checkHighlights(env, loc, 3)
})
}
@@ -54,10 +53,11 @@ func main() {
Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- file, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt\.(Printf)`))
- pos := env.RegexpSearch(file, `func Printf\((format) string`)
+ defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt\.(Printf)`))
+ file := env.Sandbox.Workdir.URIToPath(defLoc.URI)
+ loc := env.RegexpSearch(file, `func Printf\((format) string`)
- checkHighlights(env, file, pos, 2)
+ checkHighlights(env, loc, 2)
})
}
@@ -113,26 +113,28 @@ func main() {}`
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- file, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"example.com/global"`))
- pos := env.RegexpSearch(file, `const (A)`)
- checkHighlights(env, file, pos, 4)
+ defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/global"`))
+ file := env.Sandbox.Workdir.URIToPath(defLoc.URI)
+ loc := env.RegexpSearch(file, `const (A)`)
+ checkHighlights(env, loc, 4)
- file, _ = env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"example.com/local"`))
- pos = env.RegexpSearch(file, `const (b)`)
- checkHighlights(env, file, pos, 5)
+ defLoc = env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/local"`))
+ file = env.Sandbox.Workdir.URIToPath(defLoc.URI)
+ loc = env.RegexpSearch(file, `const (b)`)
+ checkHighlights(env, loc, 5)
})
}
-func checkHighlights(env *Env, file string, pos fake.Pos, highlightCount int) {
+func checkHighlights(env *Env, loc protocol.Location, highlightCount int) {
t := env.T
t.Helper()
- highlights := env.DocumentHighlight(file, pos)
+ highlights := env.DocumentHighlight(loc)
if len(highlights) != highlightCount {
t.Fatalf("expected %v highlight(s), got %v", highlightCount, len(highlights))
}
- references := env.References(file, pos)
+ references := env.References(loc)
if len(highlights) != len(references) {
t.Fatalf("number of highlights and references is expected to be equal: %v != %v", len(highlights), len(references))
}
diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go
index 04dc740b8..72a6e23eb 100644
--- a/gopls/internal/regtest/misc/hover_test.go
+++ b/gopls/internal/regtest/misc/hover_test.go
@@ -5,11 +5,13 @@
package misc
import (
+ "fmt"
"strings"
"testing"
- "golang.org/x/tools/internal/lsp/fake"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/fake"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
"golang.org/x/tools/internal/testenv"
)
@@ -59,21 +61,22 @@ func main() {
ProxyFiles(proxy),
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- mixedPos := env.RegexpSearch("main.go", "Mixed")
- got, _ := env.Hover("main.go", mixedPos)
+ mixedLoc := env.RegexpSearch("main.go", "Mixed")
+ got, _ := env.Hover(mixedLoc)
if !strings.Contains(got.Value, "unexported") {
t.Errorf("Workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value)
}
- cacheFile, _ := env.GoToDefinition("main.go", mixedPos)
- argPos := env.RegexpSearch(cacheFile, "printMixed.*(Mixed)")
- got, _ = env.Hover(cacheFile, argPos)
+ cacheLoc := env.GoToDefinition(mixedLoc)
+ cacheFile := env.Sandbox.Workdir.URIToPath(cacheLoc.URI)
+ argLoc := env.RegexpSearch(cacheFile, "printMixed.*(Mixed)")
+ got, _ = env.Hover(argLoc)
if !strings.Contains(got.Value, "unexported") {
t.Errorf("Non-workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value)
}
- exportedFieldPos := env.RegexpSearch("main.go", "Exported")
- got, _ = env.Hover("main.go", exportedFieldPos)
+ exportedFieldLoc := env.RegexpSearch("main.go", "Exported")
+ got, _ = env.Hover(exportedFieldLoc)
if !strings.Contains(got.Value, "comment") {
t.Errorf("Workspace hover: missing comment for field 'Exported'. Got:\n%q", got.Value)
}
@@ -81,7 +84,12 @@ func main() {
}
func TestHoverIntLiteral(t *testing.T) {
- testenv.NeedsGo1Point(t, 13)
+ // 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
@@ -98,13 +106,13 @@ func main() {
Run(t, source, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
hexExpected := "58190"
- got, _ := env.Hover("main.go", env.RegexpSearch("main.go", "hex"))
+ got, _ := env.Hover(env.RegexpSearch("main.go", "hex"))
if got != nil && !strings.Contains(got.Value, hexExpected) {
t.Errorf("Hover: missing expected field '%s'. Got:\n%q", hexExpected, got.Value)
}
binExpected := "73"
- got, _ = env.Hover("main.go", env.RegexpSearch("main.go", "bigBin"))
+ got, _ = env.Hover(env.RegexpSearch("main.go", "bigBin"))
if got != nil && !strings.Contains(got.Value, binExpected) {
t.Errorf("Hover: missing expected field '%s'. Got:\n%q", binExpected, got.Value)
}
@@ -113,7 +121,8 @@ func main() {
// Tests that hovering does not trigger the panic in golang/go#48249.
func TestPanicInHoverBrokenCode(t *testing.T) {
- testenv.NeedsGo1Point(t, 13)
+ // Note: this test can not be expressed as a marker test, as it must use
+ // content without a trailing newline.
const source = `
-- main.go --
package main
@@ -121,7 +130,7 @@ package main
type Example struct`
Run(t, source, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- env.Editor.Hover(env.Ctx, "main.go", env.RegexpSearch("main.go", "Example"))
+ env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "Example"))
})
}
@@ -137,6 +146,239 @@ package main
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.EditBuffer("main.go", fake.NewEdit(0, 0, 1, 0, "package main\nfunc main() {\nconst x = `\nfoo\n`\n}"))
- env.Editor.Hover(env.Ctx, "main.go", env.RegexpSearch("main.go", "foo"))
+ env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "foo"))
+ })
+}
+
+func TestHoverImport(t *testing.T) {
+ const packageDoc1 = "Package lib1 hover documentation"
+ const packageDoc2 = "Package lib2 hover documentation"
+ tests := []struct {
+ hoverPackage string
+ want string
+ wantError bool
+ }{
+ {
+ "mod.com/lib1",
+ packageDoc1,
+ false,
+ },
+ {
+ "mod.com/lib2",
+ packageDoc2,
+ false,
+ },
+ {
+ "mod.com/lib3",
+ "",
+ false,
+ },
+ {
+ "mod.com/lib4",
+ "",
+ true,
+ },
+ }
+ source := fmt.Sprintf(`
+-- go.mod --
+module mod.com
+
+go 1.12
+-- lib1/a.go --
+// %s
+package lib1
+
+const C = 1
+
+-- lib1/b.go --
+package lib1
+
+const D = 1
+
+-- lib2/a.go --
+// %s
+package lib2
+
+const E = 1
+
+-- lib3/a.go --
+package lib3
+
+const F = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib1"
+ "mod.com/lib2"
+ "mod.com/lib3"
+ "mod.com/lib4"
+)
+
+func main() {
+ println("Hello")
+}
+ `, packageDoc1, packageDoc2)
+ Run(t, source, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ for _, test := range tests {
+ got, _, err := env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", test.hoverPackage))
+ if test.wantError {
+ if err == nil {
+ t.Errorf("Hover(%q) succeeded unexpectedly", test.hoverPackage)
+ }
+ } else if !strings.Contains(got.Value, test.want) {
+ t.Errorf("Hover(%q): got:\n%q\nwant:\n%q", test.hoverPackage, got.Value, test.want)
+ }
+ }
+ })
+}
+
+// for x/tools/gopls: unhandled named anchor on the hover #57048
+func TestHoverTags(t *testing.T) {
+ const source = `
+-- go.mod --
+module mod.com
+
+go 1.19
+
+-- lib/a.go --
+
+// variety of execution modes.
+//
+// # Test package setup
+//
+// The regression test package uses a couple of uncommon patterns to reduce
+package lib
+
+-- a.go --
+ package main
+ import "mod.com/lib"
+
+ const A = 1
+
+}
+`
+ Run(t, source, func(t *testing.T, env *Env) {
+ t.Run("tags", func(t *testing.T) {
+ env.OpenFile("a.go")
+ z := env.RegexpSearch("a.go", "lib")
+ t.Logf("%#v", z)
+ got, _ := env.Hover(env.RegexpSearch("a.go", "lib"))
+ if strings.Contains(got.Value, "{#hdr-") {
+ t.Errorf("Hover: got {#hdr- tag:\n%q", got)
+ }
+ })
+ })
+}
+
+// This is a regression test for Go issue #57625.
+func TestHoverModMissingModuleStmt(t *testing.T) {
+ const source = `
+-- go.mod --
+go 1.16
+`
+ Run(t, source, func(t *testing.T, env *Env) {
+ env.OpenFile("go.mod")
+ env.Hover(env.RegexpSearch("go.mod", "go")) // no panic
+ })
+}
+
+func TestHoverCompletionMarkdown(t *testing.T) {
+ testenv.NeedsGo1Point(t, 19)
+ const source = `
+-- go.mod --
+module mod.com
+go 1.19
+-- main.go --
+package main
+// Just says [hello].
+//
+// [hello]: https://en.wikipedia.org/wiki/Hello
+func Hello() string {
+ Hello() //Here
+ return "hello"
+}
+`
+ Run(t, source, func(t *testing.T, env *Env) {
+ // Hover, Completion, and SignatureHelp should all produce markdown
+ // check that the markdown for SignatureHelp and Completion are
+ // the same, and contained in that for Hover (up to trailing \n)
+ env.OpenFile("main.go")
+ loc := env.RegexpSearch("main.go", "func (Hello)")
+ hover, _ := env.Hover(loc)
+ hoverContent := hover.Value
+
+ loc = env.RegexpSearch("main.go", "//Here")
+ loc.Range.Start.Character -= 3 // Hello(_) //Here
+ completions := env.Completion(loc)
+ signatures := env.SignatureHelp(loc)
+
+ if len(completions.Items) != 1 {
+ t.Errorf("got %d completions, expected 1", len(completions.Items))
+ }
+ if len(signatures.Signatures) != 1 {
+ t.Errorf("got %d signatures, expected 1", len(signatures.Signatures))
+ }
+ item := completions.Items[0].Documentation.Value
+ var itemContent string
+ if x, ok := item.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown {
+ t.Fatalf("%#v is not markdown", item)
+ } else {
+ itemContent = strings.Trim(x.Value, "\n")
+ }
+ sig := signatures.Signatures[0].Documentation.Value
+ var sigContent string
+ if x, ok := sig.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown {
+ t.Fatalf("%#v is not markdown", item)
+ } else {
+ sigContent = x.Value
+ }
+ if itemContent != sigContent {
+ t.Errorf("item:%q not sig:%q", itemContent, sigContent)
+ }
+ if !strings.Contains(hoverContent, itemContent) {
+ t.Errorf("hover:%q does not containt sig;%q", hoverContent, sigContent)
+ }
})
}
+
+// Test that the generated markdown contains links for Go references.
+// https://github.com/golang/go/issues/58352
+func TestHoverLinks(t *testing.T) {
+ testenv.NeedsGo1Point(t, 19)
+ const input = `
+-- go.mod --
+go 1.19
+module mod.com
+-- main.go --
+package main
+// [fmt]
+var A int
+// [fmt.Println]
+var B int
+// [golang.org/x/tools/go/packages.Package.String]
+var C int
+`
+ var tests = []struct {
+ pat string
+ ans string
+ }{
+ {"A", "fmt"},
+ {"B", "fmt#Println"},
+ {"C", "golang.org/x/tools/go/packages#Package.String"},
+ }
+ for _, test := range tests {
+ Run(t, input, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ loc := env.RegexpSearch("main.go", test.pat)
+ hover, _ := env.Hover(loc)
+ hoverContent := hover.Value
+ want := fmt.Sprintf("%s/%s", "https://pkg.go.dev", test.ans)
+ if !strings.Contains(hoverContent, want) {
+ t.Errorf("hover:%q does not contain link %q", hoverContent, want)
+ }
+ })
+ }
+}
diff --git a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go
index d5b6bcf43..30986ba50 100644
--- a/gopls/internal/regtest/misc/import_test.go
+++ b/gopls/internal/regtest/misc/import_test.go
@@ -8,10 +8,10 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/internal/lsp/command"
- "golang.org/x/tools/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
- "golang.org/x/tools/internal/lsp/tests"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
)
func TestAddImport(t *testing.T) {
@@ -49,9 +49,9 @@ func main() {
Command: "gopls.add_import",
Arguments: cmd.Arguments,
}, nil)
- got := env.Editor.BufferText("main.go")
+ got := env.BufferText("main.go")
if got != want {
- t.Fatalf("gopls.add_import failed\n%s", tests.Diff(t, want, got))
+ t.Fatalf("gopls.add_import failed\n%s", compare.Text(want, got))
}
})
}
diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go
index 4ae2be6bf..bea955220 100644
--- a/gopls/internal/regtest/misc/imports_test.go
+++ b/gopls/internal/regtest/misc/imports_test.go
@@ -11,9 +11,9 @@ import (
"strings"
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
- "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/internal/testenv"
)
@@ -47,7 +47,7 @@ func TestZ(t *testing.T) {
Run(t, needs, func(t *testing.T, env *Env) {
env.CreateBuffer("a_test.go", ntest)
env.SaveBuffer("a_test.go")
- got := env.Editor.BufferText("a_test.go")
+ got := env.BufferText("a_test.go")
if want != got {
t.Errorf("got\n%q, wanted\n%q", got, want)
}
@@ -76,7 +76,7 @@ func main() {
env.OrganizeImports("main.go")
actions := env.CodeAction("main.go", nil)
if len(actions) > 0 {
- got := env.Editor.BufferText("main.go")
+ got := env.BufferText("main.go")
t.Errorf("unexpected actions %#v", actions)
if got == vim1 {
t.Errorf("no changes")
@@ -146,23 +146,21 @@ import "example.com/x"
var _, _ = x.X, y.Y
`
- testenv.NeedsGo1Point(t, 15)
-
modcache, err := ioutil.TempDir("", "TestGOMODCACHE-modcache")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(modcache)
- editorConfig := EditorConfig{Env: map[string]string{"GOMODCACHE": modcache}}
WithOptions(
- editorConfig,
+ EnvVars{"GOMODCACHE": modcache},
ProxyFiles(proxy),
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- env.Await(env.DiagnosticAtRegexp("main.go", `y.Y`))
+ env.AfterChange(Diagnostics(env.AtRegexp("main.go", `y.Y`)))
env.SaveBuffer("main.go")
- env.Await(EmptyDiagnostics("main.go"))
- path, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `y.(Y)`))
+ env.AfterChange(NoDiagnostics(ForFile("main.go")))
+ loc := env.GoToDefinition(env.RegexpSearch("main.go", `y.(Y)`))
+ path := env.Sandbox.Workdir.URIToPath(loc.URI)
if !strings.HasPrefix(path, filepath.ToSlash(modcache)) {
t.Errorf("found module dependency outside of GOMODCACHE: got %v, wanted subdir of %v", path, filepath.ToSlash(modcache))
}
@@ -202,15 +200,59 @@ func TestA(t *testing.T) {
Run(t, pkg, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
var d protocol.PublishDiagnosticsParams
- env.Await(
- OnceMet(
- env.DiagnosticAtRegexp("a/a.go", "os.Stat"),
- ReadDiagnostics("a/a.go", &d),
- ),
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("a/a.go", "os.Stat")),
+ ReadDiagnostics("a/a.go", &d),
)
env.ApplyQuickFixes("a/a.go", d.Diagnostics)
- env.Await(
- EmptyDiagnostics("a/a.go"),
+ env.AfterChange(
+ NoDiagnostics(ForFile("a/a.go")),
)
})
}
+
+// Test for golang/go#52784
+func TestGoWorkImports(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+ const pkg = `
+-- go.work --
+go 1.19
+
+use (
+ ./caller
+ ./mod
+)
+-- caller/go.mod --
+module caller.com
+
+go 1.18
+
+require mod.com v0.0.0
+
+replace mod.com => ../mod
+-- caller/caller.go --
+package main
+
+func main() {
+ a.Test()
+}
+-- mod/go.mod --
+module mod.com
+
+go 1.18
+-- mod/a/a.go --
+package a
+
+func Test() {
+}
+`
+ Run(t, pkg, func(t *testing.T, env *Env) {
+ env.OpenFile("caller/caller.go")
+ env.AfterChange(Diagnostics(env.AtRegexp("caller/caller.go", "a.Test")))
+
+ // Saving caller.go should trigger goimports, which should find a.Test in
+ // the mod.com module, thanks to the go.work file.
+ env.SaveBuffer("caller/caller.go")
+ env.AfterChange(NoDiagnostics(ForFile("caller/caller.go")))
+ })
+}
diff --git a/gopls/internal/regtest/misc/leak_test.go b/gopls/internal/regtest/misc/leak_test.go
new file mode 100644
index 000000000..586ffcc41
--- /dev/null
+++ b/gopls/internal/regtest/misc/leak_test.go
@@ -0,0 +1,89 @@
+// 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/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go
index daea74250..8a64c54e2 100644
--- a/gopls/internal/regtest/misc/link_test.go
+++ b/gopls/internal/regtest/misc/link_test.go
@@ -8,13 +8,10 @@ import (
"strings"
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
-
- "golang.org/x/tools/internal/testenv"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestHoverAndDocumentLink(t *testing.T) {
- testenv.NeedsGo1Point(t, 13)
const program = `
-- go.mod --
module mod.test
@@ -31,6 +28,8 @@ package main
import "import.test/pkg"
func main() {
+ // Issue 43990: this is not a link that most users can open from an LSP
+ // client: mongodb://not.a.link.com
println(pkg.Hello)
}`
@@ -50,36 +49,38 @@ const Hello = "Hello"
env.OpenFile("main.go")
env.OpenFile("go.mod")
- modLink := "https://pkg.go.dev/mod/import.test@v1.2.3?utm_source=gopls"
- pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg?utm_source=gopls"
+ modLink := "https://pkg.go.dev/mod/import.test@v1.2.3"
+ pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg"
// First, check that we get the expected links via hover and documentLink.
- content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello"))
+ content, _ := env.Hover(env.RegexpSearch("main.go", "pkg.Hello"))
if content == nil || !strings.Contains(content.Value, pkgLink) {
t.Errorf("hover: got %v in main.go, want contains %q", content, pkgLink)
}
- content, _ = env.Hover("go.mod", env.RegexpSearch("go.mod", "import.test"))
+ content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test"))
if content == nil || !strings.Contains(content.Value, pkgLink) {
t.Errorf("hover: got %v in go.mod, want contains %q", content, pkgLink)
}
links := env.DocumentLink("main.go")
if len(links) != 1 || links[0].Target != pkgLink {
- t.Errorf("documentLink: got %v for main.go, want link to %q", links, pkgLink)
+ t.Errorf("documentLink: got links %+v for main.go, want one link with target %q", links, pkgLink)
}
links = env.DocumentLink("go.mod")
if len(links) != 1 || links[0].Target != modLink {
- t.Errorf("documentLink: got %v for go.mod, want link to %q", links, modLink)
+ t.Errorf("documentLink: got links %+v for go.mod, want one link with target %q", links, modLink)
}
// Then change the environment to make these links private.
- env.ChangeEnv(map[string]string{"GOPRIVATE": "import.test"})
+ cfg := env.Editor.Config()
+ cfg.Env = map[string]string{"GOPRIVATE": "import.test"}
+ env.ChangeConfiguration(cfg)
// Finally, verify that the links are gone.
- content, _ = env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello"))
+ content, _ = env.Hover(env.RegexpSearch("main.go", "pkg.Hello"))
if content == nil || strings.Contains(content.Value, pkgLink) {
t.Errorf("hover: got %v in main.go, want non-empty hover without %q", content, pkgLink)
}
- content, _ = env.Hover("go.mod", env.RegexpSearch("go.mod", "import.test"))
+ content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test"))
if content == nil || strings.Contains(content.Value, modLink) {
t.Errorf("hover: got %v in go.mod, want contains %q", content, modLink)
}
diff --git a/gopls/internal/regtest/misc/misc_test.go b/gopls/internal/regtest/misc/misc_test.go
index 3694b07fc..12aea697c 100644
--- a/gopls/internal/regtest/misc/misc_test.go
+++ b/gopls/internal/regtest/misc/misc_test.go
@@ -8,9 +8,11 @@ import (
"testing"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/internal/bug"
)
func TestMain(m *testing.M) {
+ bug.PanicOnBugs = true
regtest.Main(m, hooks.Options)
}
diff --git a/gopls/internal/regtest/misc/multiple_adhoc_test.go b/gopls/internal/regtest/misc/multiple_adhoc_test.go
index 5f803e4e3..981b74efc 100644
--- a/gopls/internal/regtest/misc/multiple_adhoc_test.go
+++ b/gopls/internal/regtest/misc/multiple_adhoc_test.go
@@ -7,7 +7,7 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestMultipleAdHocPackages(t *testing.T) {
@@ -30,14 +30,14 @@ func main() () {
}
`, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
- if list := env.Completion("a/a.go", env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 {
+ if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 {
t.Fatal("expected completions, got none")
}
env.OpenFile("a/b.go")
- if list := env.Completion("a/b.go", env.RegexpSearch("a/b.go", "Println")); list == nil || len(list.Items) == 0 {
+ if list := env.Completion(env.RegexpSearch("a/b.go", "Println")); list == nil || len(list.Items) == 0 {
t.Fatal("expected completions, got none")
}
- if list := env.Completion("a/a.go", env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 {
+ if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 {
t.Fatal("expected completions, got none")
}
})
diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go
index 768251680..e1f5d8e05 100644
--- a/gopls/internal/regtest/misc/references_test.go
+++ b/gopls/internal/regtest/misc/references_test.go
@@ -5,9 +5,15 @@
package misc
import (
+ "fmt"
+ "os"
+ "sort"
+ "strings"
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestStdlibReferences(t *testing.T) {
@@ -28,12 +34,13 @@ func main() {
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Print)`))
- refs, err := env.Editor.References(env.Ctx, file, pos)
+ loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Print)`))
+ refs, err := env.Editor.References(env.Ctx, loc)
if err != nil {
t.Fatal(err)
}
if len(refs) != 2 {
+ // TODO(adonovan): make this assertion less maintainer-hostile.
t.Fatalf("got %v reference(s), want 2", len(refs))
}
// The first reference is guaranteed to be the definition.
@@ -43,8 +50,10 @@ func main() {
})
}
-// This reproduces and tests golang/go#48400.
-func TestReferencesPanicOnError(t *testing.T) {
+// This is a regression test for golang/go#48400 (a panic).
+func TestReferencesOnErrorMethod(t *testing.T) {
+ // Ideally this would actually return the correct answer,
+ // instead of merely failing gracefully.
const files = `
-- go.mod --
module mod.com
@@ -70,14 +79,321 @@ func _() {
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `Error`))
- refs, err := env.Editor.References(env.Ctx, file, pos)
- if err == nil {
- t.Fatalf("expected error for references, instead got %v", refs)
+ loc := env.GoToDefinition(env.RegexpSearch("main.go", `Error`))
+ refs, err := env.Editor.References(env.Ctx, loc)
+ if err != nil {
+ t.Fatalf("references on (*s).Error failed: %v", err)
+ }
+ // TODO(adonovan): this test is crying out for marker support in regtests.
+ var buf strings.Builder
+ for _, ref := range refs {
+ fmt.Fprintf(&buf, "%s %s\n", env.Sandbox.Workdir.URIToPath(ref.URI), ref.Range)
}
- wantErr := "no position for func (error).Error() string"
- if err.Error() != wantErr {
- t.Fatalf("expected error with message %s, instead got %s", wantErr, err.Error())
+ got := buf.String()
+ want := "main.go 8:10-8:15\n" + // (*s).Error decl
+ "main.go 14:7-14:12\n" // s.Error() call
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("unexpected references on (*s).Error (-want +got):\n%s", diff)
+ }
+ })
+}
+
+func TestPackageReferences(t *testing.T) {
+ tests := []struct {
+ packageName string
+ wantRefCount int
+ wantFiles []string
+ }{
+ {
+ "lib1",
+ 3,
+ []string{
+ "main.go",
+ "lib1/a.go",
+ "lib1/b.go",
+ },
+ },
+ {
+ "lib2",
+ 2,
+ []string{
+ "main.go",
+ "lib2/a.go",
+ },
+ },
+ }
+
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib1/a.go --
+package lib1
+
+const A = 1
+
+-- lib1/b.go --
+package lib1
+
+const B = 1
+
+-- lib2/a.go --
+package lib2
+
+const C = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib1"
+ "mod.com/lib2"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ for _, test := range tests {
+ file := fmt.Sprintf("%s/a.go", test.packageName)
+ env.OpenFile(file)
+ loc := env.RegexpSearch(file, test.packageName)
+ refs := env.References(loc)
+ if len(refs) != test.wantRefCount {
+ // TODO(adonovan): make this assertion less maintainer-hostile.
+ t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount)
+ }
+ var refURIs []string
+ for _, ref := range refs {
+ refURIs = append(refURIs, string(ref.URI))
+ }
+ for _, base := range test.wantFiles {
+ hasBase := false
+ for _, ref := range refURIs {
+ if strings.HasSuffix(ref, base) {
+ hasBase = true
+ break
+ }
+ }
+ if !hasBase {
+ t.Fatalf("got [%v], want reference ends with \"%v\"", strings.Join(refURIs, ","), base)
+ }
+ }
}
})
}
+
+// Test for golang/go#43144.
+//
+// Verify that we search for references and implementations in intermediate
+// test variants.
+func TestReferencesInTestVariants(t *testing.T) {
+ const files = `
+-- go.mod --
+module foo.mod
+
+go 1.12
+-- foo/foo.go --
+package foo
+
+import "foo.mod/bar"
+
+const Foo = 42
+
+type T int
+type InterfaceM interface{ M() }
+type InterfaceF interface{ F() }
+
+func _() {
+ _ = bar.Blah
+}
+
+-- foo/foo_test.go --
+package foo
+
+type Fer struct{}
+func (Fer) F() {}
+
+-- bar/bar.go --
+package bar
+
+var Blah = 123
+
+-- bar/bar_test.go --
+package bar
+
+type Mer struct{}
+func (Mer) M() {}
+
+func TestBar() {
+ _ = Blah
+}
+-- bar/bar_x_test.go --
+package bar_test
+
+import (
+ "foo.mod/bar"
+ "foo.mod/foo"
+)
+
+type Mer struct{}
+func (Mer) M() {}
+
+func _() {
+ _ = bar.Blah
+ _ = foo.Foo
+}
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("foo/foo.go")
+
+ // Helper to map locations relative file paths.
+ fileLocations := func(locs []protocol.Location) []string {
+ var got []string
+ for _, loc := range locs {
+ got = append(got, env.Sandbox.Workdir.URIToPath(loc.URI))
+ }
+ sort.Strings(got)
+ return got
+ }
+
+ refTests := []struct {
+ re string
+ wantRefs []string
+ }{
+ // Blah is referenced:
+ // - inside the foo.mod/bar (ordinary) package
+ // - inside the foo.mod/bar [foo.mod/bar.test] test variant package
+ // - from the foo.mod/bar_test [foo.mod/bar.test] x_test package
+ // - from the foo.mod/foo package
+ {"Blah", []string{"bar/bar.go", "bar/bar_test.go", "bar/bar_x_test.go", "foo/foo.go"}},
+
+ // Foo is referenced in bar_x_test.go via the intermediate test variant
+ // foo.mod/foo [foo.mod/bar.test].
+ {"Foo", []string{"bar/bar_x_test.go", "foo/foo.go"}},
+ }
+
+ for _, test := range refTests {
+ loc := env.RegexpSearch("foo/foo.go", test.re)
+ refs := env.References(loc)
+
+ got := fileLocations(refs)
+ if diff := cmp.Diff(test.wantRefs, got); diff != "" {
+ t.Errorf("References(%q) returned unexpected diff (-want +got):\n%s", test.re, diff)
+ }
+ }
+
+ implTests := []struct {
+ re string
+ wantImpls []string
+ }{
+ // InterfaceM is implemented both in foo.mod/bar [foo.mod/bar.test] (which
+ // doesn't import foo), and in foo.mod/bar_test [foo.mod/bar.test], which
+ // imports the test variant of foo.
+ {"InterfaceM", []string{"bar/bar_test.go", "bar/bar_x_test.go"}},
+
+ // A search within the ordinary package to should find implementations
+ // (Fer) within the augmented test package.
+ {"InterfaceF", []string{"foo/foo_test.go"}},
+ }
+
+ for _, test := range implTests {
+ loc := env.RegexpSearch("foo/foo.go", test.re)
+ impls := env.Implementations(loc)
+
+ got := fileLocations(impls)
+ if diff := cmp.Diff(test.wantImpls, got); diff != "" {
+ t.Errorf("Implementations(%q) returned unexpected diff (-want +got):\n%s", test.re, diff)
+ }
+ }
+ })
+}
+
+// This is a regression test for Issue #56169, in which interface
+// implementations in vendored modules were not found. The actual fix
+// was the same as for #55995; see TestVendoringInvalidatesMetadata.
+func TestImplementationsInVendor(t *testing.T) {
+ t.Skip("golang/go#56169: file watching does not capture vendor dirs")
+
+ const proxy = `
+-- other.com/b@v1.0.0/go.mod --
+module other.com/b
+go 1.14
+
+-- other.com/b@v1.0.0/b.go --
+package b
+type B int
+func (B) F() {}
+`
+ const src = `
+-- go.mod --
+module example.com/a
+go 1.14
+require other.com/b v1.0.0
+
+-- go.sum --
+other.com/b v1.0.0 h1:9WyCKS+BLAMRQM0CegP6zqP2beP+ShTbPaARpNY31II=
+other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g=
+
+-- a.go --
+package a
+import "other.com/b"
+type I interface { F() }
+var _ b.B
+
+`
+ WithOptions(
+ ProxyFiles(proxy),
+ Modes(Default), // fails in 'experimental' mode
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ // Enable to debug go.sum mismatch, which may appear as
+ // "module lookup disabled by GOPROXY=off", confusingly.
+ if false {
+ env.DumpGoSum(".")
+ }
+
+ checkVendor := func(locs []protocol.Location, wantVendor bool) {
+ if len(locs) != 1 {
+ t.Errorf("got %d locations, want 1", len(locs))
+ } else if strings.Contains(string(locs[0].URI), "/vendor/") != wantVendor {
+ t.Errorf("got location %s, wantVendor=%t", locs[0], wantVendor)
+ }
+ }
+
+ env.OpenFile("a.go")
+ refLoc := env.RegexpSearch("a.go", "I") // find "I" reference
+
+ // Initially, a.I has one implementation b.B in
+ // the module cache, not the vendor tree.
+ checkVendor(env.Implementations(refLoc), false)
+
+ // Run 'go mod vendor' outside the editor.
+ if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, true); err != nil {
+ t.Fatalf("go mod vendor: %v", err)
+ }
+
+ // Synchronize changes to watched files.
+ env.Await(env.DoneWithChangeWatchedFiles())
+
+ // Now, b.B is found in the vendor tree.
+ checkVendor(env.Implementations(refLoc), true)
+
+ // Delete the vendor tree.
+ if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil {
+ t.Fatal(err)
+ }
+ // Notify the server of the deletion.
+ if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil {
+ t.Fatal(err)
+ }
+
+ // Synchronize again.
+ env.Await(env.DoneWithChangeWatchedFiles())
+
+ // b.B is once again defined in the module cache.
+ checkVendor(env.Implementations(refLoc), false)
+ })
+}
diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go
index 121b70725..ebb02609d 100644
--- a/gopls/internal/regtest/misc/rename_test.go
+++ b/gopls/internal/regtest/misc/rename_test.go
@@ -5,12 +5,319 @@
package misc
import (
+ "fmt"
"strings"
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
+ "golang.org/x/tools/internal/testenv"
+)
+
+func TestPrepareRenameMainPackage(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- main.go --
+package main
+
+import (
+ "fmt"
+)
+
+func main() {
+ fmt.Println(1)
+}
+`
+ const wantErr = "can't rename package \"main\""
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ loc := env.RegexpSearch("main.go", `main`)
+ params := &protocol.PrepareRenameParams{
+ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc),
+ }
+ _, err := env.Editor.Server.PrepareRename(env.Ctx, params)
+ if err == nil {
+ t.Errorf("missing can't rename package main error from PrepareRename")
+ }
+
+ if err.Error() != wantErr {
+ t.Errorf("got %v, want %v", err.Error(), wantErr)
+ }
+ })
+}
+
+// Test case for golang/go#56227
+func TestRenameWithUnsafeSlice(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17) // unsafe.Slice was added in Go 1.17
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- p.go --
+package p
+
+import "unsafe"
+
+type T struct{}
+
+func (T) M() {}
+
+func _() {
+ x := [3]int{1, 2, 3}
+ ptr := unsafe.Pointer(&x)
+ _ = unsafe.Slice((*int)(ptr), 3)
+}
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("p.go")
+ env.Rename(env.RegexpSearch("p.go", "M"), "N") // must not panic
+ })
+}
+
+func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) {
+ const files = `
+go 1.14
+-- lib/a.go --
+import "fmt"
+
+const A = 1
+
+func bar() {
+ fmt.Println("Bar")
+}
+
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ err := env.Editor.Rename(env.Ctx, env.RegexpSearch("lib/a.go", "fmt"), "fmt1")
+ if got, want := fmt.Sprint(err), "no identifier found"; got != want {
+ t.Errorf("Rename: got error %v, want %v", got, want)
+ }
+ })
+}
+
+func TestPrepareRenameFailWithUnknownModule(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+go 1.14
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ const wantErr = "can't rename package: missing module information for package"
+ Run(t, files, func(t *testing.T, env *Env) {
+ loc := env.RegexpSearch("lib/a.go", "lib")
+ params := &protocol.PrepareRenameParams{
+ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc),
+ }
+ _, err := env.Editor.Server.PrepareRename(env.Ctx, params)
+ if err == nil || !strings.Contains(err.Error(), wantErr) {
+ t.Errorf("missing cannot rename packages with unknown module from PrepareRename")
+ }
+ })
+}
+
+// This test ensures that each import of a renamed package
+// is also renamed if it would otherwise create a conflict.
+func TestRenamePackageWithConflicts(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- lib/nested/a.go --
+package nested
+
+const B = 1
+
+-- lib/x/a.go --
+package nested1
+
+const C = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+ "mod.com/lib/nested"
+ nested1 "mod.com/lib/x"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("nested/a.go", "package nested")
+ env.RegexpSearch("main.go", `nested2 "mod.com/nested"`)
+ env.RegexpSearch("main.go", "mod.com/nested/nested")
+ env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`)
+ })
+}
+
+func TestRenamePackageWithAlias(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- lib/nested/a.go --
+package nested
+
+const B = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+ lib1 "mod.com/lib/nested"
)
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("nested/a.go", "package nested")
+ env.RegexpSearch("main.go", "mod.com/nested")
+ env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`)
+ })
+}
+
+func TestRenamePackageWithDifferentDirectoryPath(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- lib/nested/a.go --
+package foo
+
+const B = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+ foo "mod.com/lib/nested"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("nested/a.go", "package nested")
+ env.RegexpSearch("main.go", "mod.com/nested")
+ env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`)
+ })
+}
+
+func TestRenamePackage(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- lib/b.go --
+package lib
+
+const B = 1
+
+-- lib/nested/a.go --
+package nested
+
+const C = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+ "mod.com/lib/nested"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("lib1/a.go", "package lib1")
+ env.RegexpSearch("lib1/b.go", "package lib1")
+ env.RegexpSearch("main.go", "mod.com/lib1")
+ env.RegexpSearch("main.go", "mod.com/lib1/nested")
+ })
+}
+
// Test for golang/go#47564.
func TestRenameInTestVariant(t *testing.T) {
const files = `
@@ -48,11 +355,581 @@ func main() {
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- pos := env.RegexpSearch("main.go", `stringutil\.(Identity)`)
- env.Rename("main.go", pos, "Identityx")
- text := env.Editor.BufferText("stringutil/stringutil_test.go")
+ env.Rename(env.RegexpSearch("main.go", `stringutil\.(Identity)`), "Identityx")
+ env.OpenFile("stringutil/stringutil_test.go")
+ text := env.BufferText("stringutil/stringutil_test.go")
if !strings.Contains(text, "Identityx") {
t.Errorf("stringutil/stringutil_test.go: missing expected token `Identityx` after rename:\n%s", text)
}
})
}
+
+// This is a test that rename operation initiated by the editor function as expected.
+func TestRenameFileFromEditor(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.16
+-- a/a.go --
+package a
+
+const X = 1
+-- a/x.go --
+package a
+
+const X = 2
+-- b/b.go --
+package b
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ // Rename files and verify that diagnostics are affected accordingly.
+
+ // Initially, we should have diagnostics on both X's, for their duplicate declaration.
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ Diagnostics(env.AtRegexp("a/a.go", "X")),
+ Diagnostics(env.AtRegexp("a/x.go", "X")),
+ )
+
+ // Moving x.go should make the diagnostic go away.
+ env.RenameFile("a/x.go", "b/x.go")
+ env.AfterChange(
+ NoDiagnostics(ForFile("a/a.go")), // no more duplicate declarations
+ Diagnostics(env.AtRegexp("b/b.go", "package")), // as package names mismatch
+ )
+
+ // Renaming should also work on open buffers.
+ env.OpenFile("b/x.go")
+
+ // Moving x.go back to a/ should cause the diagnostics to reappear.
+ env.RenameFile("b/x.go", "a/x.go")
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("a/a.go", "X")),
+ Diagnostics(env.AtRegexp("a/x.go", "X")),
+ )
+
+ // Renaming the entire directory should move both the open and closed file.
+ env.RenameFile("a", "x")
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("x/a.go", "X")),
+ Diagnostics(env.AtRegexp("x/x.go", "X")),
+ )
+
+ // As a sanity check, verify that x/x.go is open.
+ if text := env.BufferText("x/x.go"); text == "" {
+ t.Fatal("got empty buffer for x/x.go")
+ }
+ })
+}
+
+func TestRenamePackage_Tests(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- lib/b.go --
+package lib
+
+const B = 1
+
+-- lib/a_test.go --
+package lib_test
+
+import (
+ "mod.com/lib"
+ "fmt
+)
+
+const C = 1
+
+-- lib/b_test.go --
+package lib
+
+import (
+ "fmt
+)
+
+const D = 1
+
+-- lib/nested/a.go --
+package nested
+
+const D = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+ "mod.com/lib/nested"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("lib1/a.go", "package lib1")
+ env.RegexpSearch("lib1/b.go", "package lib1")
+ env.RegexpSearch("main.go", "mod.com/lib1")
+ env.RegexpSearch("main.go", "mod.com/lib1/nested")
+
+ // Check if the test package is renamed
+ env.RegexpSearch("lib1/a_test.go", "package lib1_test")
+ env.RegexpSearch("lib1/b_test.go", "package lib1")
+ })
+}
+
+func TestRenamePackage_NestedModule(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+ const files = `
+-- go.work --
+go 1.18
+use (
+ .
+ ./foo/bar
+ ./foo/baz
+)
+
+-- go.mod --
+module mod.com
+
+go 1.18
+
+require (
+ mod.com/foo/bar v0.0.0
+)
+
+replace (
+ mod.com/foo/bar => ./foo/bar
+ mod.com/foo/baz => ./foo/baz
+)
+-- foo/foo.go --
+package foo
+
+import "fmt"
+
+func Bar() {
+ fmt.Println("In foo before renamed to foox.")
+}
+
+-- foo/bar/go.mod --
+module mod.com/foo/bar
+
+-- foo/bar/bar.go --
+package bar
+
+const Msg = "Hi from package bar"
+
+-- foo/baz/go.mod --
+module mod.com/foo/baz
+
+-- foo/baz/baz.go --
+package baz
+
+const Msg = "Hi from package baz"
+
+-- main.go --
+package main
+
+import (
+ "fmt"
+ "mod.com/foo/bar"
+ "mod.com/foo/baz"
+ "mod.com/foo"
+)
+
+func main() {
+ foo.Bar()
+ fmt.Println(bar.Msg)
+ fmt.Println(baz.Msg)
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("foo/foo.go")
+ env.Rename(env.RegexpSearch("foo/foo.go", "foo"), "foox")
+
+ env.RegexpSearch("foox/foo.go", "package foox")
+ env.OpenFile("foox/bar/bar.go")
+ env.OpenFile("foox/bar/go.mod")
+
+ env.RegexpSearch("main.go", "mod.com/foo/bar")
+ env.RegexpSearch("main.go", "mod.com/foox")
+ env.RegexpSearch("main.go", "foox.Bar()")
+
+ env.RegexpSearch("go.mod", "./foox/bar")
+ env.RegexpSearch("go.mod", "./foox/baz")
+ })
+}
+
+func TestRenamePackage_DuplicateImport(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- lib/nested/a.go --
+package nested
+
+const B = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+ lib1 "mod.com/lib"
+ lib2 "mod.com/lib/nested"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("nested/a.go", "package nested")
+ env.RegexpSearch("main.go", "mod.com/nested")
+ env.RegexpSearch("main.go", `lib1 "mod.com/nested"`)
+ env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`)
+ })
+}
+
+func TestRenamePackage_DuplicateBlankImport(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+const A = 1
+
+-- lib/nested/a.go --
+package nested
+
+const B = 1
+
+-- main.go --
+package main
+
+import (
+ "mod.com/lib"
+ _ "mod.com/lib"
+ lib1 "mod.com/lib/nested"
+)
+
+func main() {
+ println("Hello")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("nested/a.go", "package nested")
+ env.RegexpSearch("main.go", "mod.com/nested")
+ env.RegexpSearch("main.go", `_ "mod.com/nested"`)
+ env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`)
+ })
+}
+
+func TestRenamePackage_TestVariant(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- foo/foo.go --
+package foo
+
+const Foo = 42
+-- bar/bar.go --
+package bar
+
+import "mod.com/foo"
+
+const Bar = foo.Foo
+-- bar/bar_test.go --
+package bar
+
+import "mod.com/foo"
+
+const Baz = foo.Foo
+-- testdata/bar/bar.go --
+package bar
+
+import "mod.com/foox"
+
+const Bar = foox.Foo
+-- testdata/bar/bar_test.go --
+package bar
+
+import "mod.com/foox"
+
+const Baz = foox.Foo
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("foo/foo.go")
+ env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox")
+
+ checkTestdata(t, env)
+ })
+}
+
+func TestRenamePackage_IntermediateTestVariant(t *testing.T) {
+ // In this test set up, we have the following import edges:
+ // bar_test -> baz -> foo -> bar
+ // bar_test -> foo -> bar
+ // bar_test -> bar
+ //
+ // As a consequence, bar_x_test.go is in the reverse closure of both
+ // `foo [bar.test]` and `baz [bar.test]`. This test confirms that we don't
+ // produce duplicate edits in this case.
+ const files = `
+-- go.mod --
+module foo.mod
+
+go 1.12
+-- foo/foo.go --
+package foo
+
+import "foo.mod/bar"
+
+const Foo = 42
+
+const _ = bar.Bar
+-- baz/baz.go --
+package baz
+
+import "foo.mod/foo"
+
+const Baz = foo.Foo
+-- bar/bar.go --
+package bar
+
+var Bar = 123
+-- bar/bar_test.go --
+package bar
+
+const _ = Bar
+-- bar/bar_x_test.go --
+package bar_test
+
+import (
+ "foo.mod/bar"
+ "foo.mod/baz"
+ "foo.mod/foo"
+)
+
+const _ = bar.Bar + baz.Baz + foo.Foo
+-- testdata/foox/foo.go --
+package foox
+
+import "foo.mod/bar"
+
+const Foo = 42
+
+const _ = bar.Bar
+-- testdata/baz/baz.go --
+package baz
+
+import "foo.mod/foox"
+
+const Baz = foox.Foo
+-- testdata/bar/bar_x_test.go --
+package bar_test
+
+import (
+ "foo.mod/bar"
+ "foo.mod/baz"
+ "foo.mod/foox"
+)
+
+const _ = bar.Bar + baz.Baz + foox.Foo
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("foo/foo.go")
+ env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox")
+
+ checkTestdata(t, env)
+ })
+}
+
+func TestRenamePackage_Nesting(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+import "mod.com/lib/nested"
+
+const A = 1 + nested.B
+-- lib/nested/a.go --
+package nested
+
+const B = 1
+-- other/other.go --
+package other
+
+import (
+ "mod.com/lib"
+ "mod.com/lib/nested"
+)
+
+const C = lib.A + nested.B
+-- testdata/libx/a.go --
+package libx
+
+import "mod.com/libx/nested"
+
+const A = 1 + nested.B
+-- testdata/other/other.go --
+package other
+
+import (
+ "mod.com/libx"
+ "mod.com/libx/nested"
+)
+
+const C = libx.A + nested.B
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "package (lib)"), "libx")
+
+ checkTestdata(t, env)
+ })
+}
+
+func TestRenamePackage_InvalidName(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+import "mod.com/lib/nested"
+
+const A = 1 + nested.B
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/a.go")
+ loc := env.RegexpSearch("lib/a.go", "package (lib)")
+
+ for _, badName := range []string{"$$$", "lib_test"} {
+ if err := env.Editor.Rename(env.Ctx, loc, badName); err == nil {
+ t.Errorf("Rename(lib, libx) succeeded, want non-nil error")
+ }
+ }
+ })
+}
+
+func TestRenamePackage_InternalPackage(t *testing.T) {
+ testenv.NeedsGo1Point(t, 17)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- lib/a.go --
+package lib
+
+import (
+ "fmt"
+ "mod.com/lib/internal/x"
+)
+
+const A = 1
+
+func print() {
+ fmt.Println(x.B)
+}
+
+-- lib/internal/x/a.go --
+package x
+
+const B = 1
+
+-- main.go --
+package main
+
+import "mod.com/lib"
+
+func main() {
+ lib.print()
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("lib/internal/x/a.go")
+ env.Rename(env.RegexpSearch("lib/internal/x/a.go", "x"), "utils")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("lib/a.go", "mod.com/lib/internal/utils")
+ env.RegexpSearch("lib/a.go", "utils.B")
+
+ // Check if the test package is renamed
+ env.RegexpSearch("lib/internal/utils/a.go", "package utils")
+
+ env.OpenFile("lib/a.go")
+ env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1")
+
+ // Check if the new package name exists.
+ env.RegexpSearch("lib1/a.go", "package lib1")
+ env.RegexpSearch("lib1/a.go", "mod.com/lib1/internal/utils")
+ env.RegexpSearch("main.go", `import "mod.com/lib1"`)
+ env.RegexpSearch("main.go", "lib1.print()")
+ })
+}
+
+// checkTestdata checks that current buffer contents match their corresponding
+// expected content in the testdata directory.
+func checkTestdata(t *testing.T, env *Env) {
+ t.Helper()
+ files := env.ListFiles("testdata")
+ if len(files) == 0 {
+ t.Fatal("no files in testdata directory")
+ }
+ for _, file := range files {
+ suffix := strings.TrimPrefix(file, "testdata/")
+ got := env.BufferText(suffix)
+ want := env.ReadWorkspaceFile(file)
+ if diff := compare.Text(want, got); diff != "" {
+ t.Errorf("Rename: unexpected buffer content for %s (-want +got):\n%s", suffix, diff)
+ }
+ }
+}
diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go
index 79507876a..a96024b9c 100644
--- a/gopls/internal/regtest/misc/semantictokens_test.go
+++ b/gopls/internal/regtest/misc/semantictokens_test.go
@@ -5,10 +5,14 @@
package misc
import (
+ "strings"
"testing"
- "golang.org/x/tools/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/lsp"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/internal/typeparams"
)
func TestBadURICrash_VSCodeIssue1498(t *testing.T) {
@@ -25,10 +29,8 @@ func main() {}
`
WithOptions(
- Modes(Singleton),
- EditorConfig{
- AllExperiments: true,
- },
+ Modes(Default),
+ Settings{"allExperiments": true},
).Run(t, src, func(t *testing.T, env *Env) {
params := &protocol.SemanticTokensParams{}
const badURI = "http://foo"
@@ -42,3 +44,161 @@ func main() {}
}
})
}
+
+// fix bug involving type parameters and regular parameters
+// (golang/vscode-go#2527)
+func TestSemantic_2527(t *testing.T) {
+ if !typeparams.Enabled {
+ t.Skip("type parameters are needed for this test")
+ }
+ // these are the expected types of identifiers in text order
+ want := []result{
+ {"package", "keyword", ""},
+ {"foo", "namespace", ""},
+ {"func", "keyword", ""},
+ {"Add", "function", "definition deprecated"},
+ {"T", "typeParameter", "definition"},
+ {"int", "type", "defaultLibrary"},
+ {"target", "parameter", "definition"},
+ {"T", "typeParameter", ""},
+ {"l", "parameter", "definition"},
+ {"T", "typeParameter", ""},
+ {"T", "typeParameter", ""},
+ {"return", "keyword", ""},
+ {"append", "function", "defaultLibrary"},
+ {"l", "parameter", ""},
+ {"target", "parameter", ""},
+ {"for", "keyword", ""},
+ {"range", "keyword", ""},
+ {"l", "parameter", ""},
+ {"return", "keyword", ""},
+ {"nil", "variable", "readonly defaultLibrary"},
+ }
+ src := `
+-- go.mod --
+module example.com
+
+go 1.19
+-- main.go --
+package foo
+// Deprecated (for testing)
+func Add[T int](target T, l []T) []T {
+ return append(l, target)
+ for range l {} // test coverage
+ return nil
+}
+`
+ WithOptions(
+ Modes(Default),
+ Settings{"semanticTokens": true},
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("main.go", "for range")),
+ )
+ p := &protocol.SemanticTokensParams{
+ TextDocument: protocol.TextDocumentIdentifier{
+ URI: env.Sandbox.Workdir.URI("main.go"),
+ },
+ }
+ v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ seen := interpret(v.Data, env.BufferText("main.go"))
+ if x := cmp.Diff(want, seen); x != "" {
+ t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
+ }
+ })
+
+}
+
+// fix inconsistency in TypeParameters
+// https://github.com/golang/go/issues/57619
+func TestSemantic_57619(t *testing.T) {
+ if !typeparams.Enabled {
+ t.Skip("type parameters are needed for this test")
+ }
+ src := `
+-- go.mod --
+module example.com
+
+go 1.19
+-- main.go --
+package foo
+type Smap[K int, V any] struct {
+ Store map[K]V
+}
+func (s *Smap[K, V]) Get(k K) (V, bool) {
+ v, ok := s.Store[k]
+ return v, ok
+}
+func New[K int, V any]() Smap[K, V] {
+ return Smap[K, V]{Store: make(map[K]V)}
+}
+`
+ WithOptions(
+ Modes(Default),
+ Settings{"semanticTokens": true},
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ p := &protocol.SemanticTokensParams{
+ TextDocument: protocol.TextDocumentIdentifier{
+ URI: env.Sandbox.Workdir.URI("main.go"),
+ },
+ }
+ v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ seen := interpret(v.Data, env.BufferText("main.go"))
+ for i, s := range seen {
+ if (s.Token == "K" || s.Token == "V") && s.TokenType != "typeParameter" {
+ t.Errorf("%d: expected K and V to be type parameters, but got %v", i, s)
+ }
+ }
+ })
+}
+
+type result struct {
+ Token string
+ TokenType string
+ Mod string
+}
+
+// human-readable version of the semantic tokens
+// comment, string, number are elided
+// (and in the future, maybe elide other things, like operators)
+func interpret(x []uint32, contents string) []result {
+ lines := strings.Split(contents, "\n")
+ ans := []result{}
+ line, col := 1, 1
+ for i := 0; i < len(x); i += 5 {
+ line += int(x[i])
+ col += int(x[i+1])
+ if x[i] != 0 { // new line
+ col = int(x[i+1]) + 1 // 1-based column numbers
+ }
+ sz := x[i+2]
+ t := semanticTypes[x[i+3]]
+ if t == "comment" || t == "string" || t == "number" {
+ continue
+ }
+ l := x[i+4]
+ var mods []string
+ for i, mod := range semanticModifiers {
+ if l&(1<<i) != 0 {
+ mods = append(mods, mod)
+ }
+ }
+ // col is a utf-8 offset
+ tok := lines[line-1][col-1 : col-1+int(sz)]
+ ans = append(ans, result{tok, t, strings.Join(mods, " ")})
+ }
+ return ans
+}
+
+var (
+ semanticTypes = lsp.SemanticTypes()
+ semanticModifiers = lsp.SemanticModifiers()
+)
diff --git a/gopls/internal/regtest/misc/settings_test.go b/gopls/internal/regtest/misc/settings_test.go
index 7704c3c04..dd4042989 100644
--- a/gopls/internal/regtest/misc/settings_test.go
+++ b/gopls/internal/regtest/misc/settings_test.go
@@ -7,7 +7,7 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
func TestEmptyDirectoryFilters_Issue51843(t *testing.T) {
@@ -24,11 +24,7 @@ func main() {
`
WithOptions(
- EditorConfig{
- Settings: map[string]interface{}{
- "directoryFilters": []string{""},
- },
- },
+ Settings{"directoryFilters": []string{""}},
).Run(t, src, func(t *testing.T, env *Env) {
// No need to do anything. Issue golang/go#51843 is triggered by the empty
// directory filter above.
diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go
index 6861743ff..410a8d327 100644
--- a/gopls/internal/regtest/misc/shared_test.go
+++ b/gopls/internal/regtest/misc/shared_test.go
@@ -7,10 +7,13 @@ package misc
import (
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/fake"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
)
-const sharedProgram = `
+// Smoke test that simultaneous editing sessions in the same workspace works.
+func TestSimultaneousEdits(t *testing.T) {
+ const sharedProgram = `
-- go.mod --
module mod
@@ -24,20 +27,26 @@ func main() {
fmt.Println("Hello World.")
}`
-func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) {
- // Only run these tests in forwarded modes.
- modes := DefaultModes() & (Forwarded | SeparateProcess)
- WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
+ WithOptions(
+ Modes(DefaultModes()&(Forwarded|SeparateProcess)),
+ ).Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
// Create a second test session connected to the same workspace and server
// as the first.
- env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true)
+ awaiter := NewAwaiter(env1.Sandbox.Workdir)
+ const skipApplyEdits = false
+ editor, err := fake.NewEditor(env1.Sandbox, env1.Editor.Config()).Connect(env1.Ctx, env1.Server, awaiter.Hooks(), skipApplyEdits)
+ if err != nil {
+ t.Fatal(err)
+ }
+ env2 := &Env{
+ T: t,
+ Ctx: env1.Ctx,
+ Sandbox: env1.Sandbox,
+ Server: env1.Server,
+ Editor: editor,
+ Awaiter: awaiter,
+ }
env2.Await(InitialWorkspaceLoad)
- testFunc(env1, env2)
- })
-}
-
-func TestSimultaneousEdits(t *testing.T) {
- runShared(t, func(env1 *Env, env2 *Env) {
// In editor #1, break fmt.Println as before.
env1.OpenFile("main.go")
env1.RegexpReplace("main.go", "Printl(n)", "")
@@ -46,19 +55,18 @@ func TestSimultaneousEdits(t *testing.T) {
env2.RegexpReplace("main.go", "\\)\n(})", "")
// Now check that we got different diagnostics in each environment.
- env1.Await(env1.DiagnosticAtRegexp("main.go", "Printl"))
- env2.Await(env2.DiagnosticAtRegexp("main.go", "$"))
- })
-}
+ env1.AfterChange(Diagnostics(env1.AtRegexp("main.go", "Printl")))
+ env2.AfterChange(Diagnostics(env2.AtRegexp("main.go", "$")))
-func TestShutdown(t *testing.T) {
- runShared(t, func(env1 *Env, env2 *Env) {
- if err := env1.Editor.Close(env1.Ctx); err != nil {
- t.Errorf("closing first editor: %v", err)
+ // Now close editor #2, and verify that operation in editor #1 is
+ // unaffected.
+ if err := env2.Editor.Close(env2.Ctx); err != nil {
+ t.Errorf("closing second editor: %v", err)
}
- // Now make an edit in editor #2 to trigger diagnostics.
- env2.OpenFile("main.go")
- env2.RegexpReplace("main.go", "\\)\n(})", "")
- env2.Await(env2.DiagnosticAtRegexp("main.go", "$"))
+
+ env1.RegexpReplace("main.go", "Printl", "Println")
+ env1.AfterChange(
+ NoDiagnostics(ForFile("main.go")),
+ )
})
}
diff --git a/gopls/internal/regtest/misc/signature_help_test.go b/gopls/internal/regtest/misc/signature_help_test.go
new file mode 100644
index 000000000..fd9f4f07a
--- /dev/null
+++ b/gopls/internal/regtest/misc/signature_help_test.go
@@ -0,0 +1,69 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package misc
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+)
+
+func TestSignatureHelpInNonWorkspacePackage(t *testing.T) {
+ const files = `
+-- a/go.mod --
+module a.com
+
+go 1.18
+-- a/a/a.go --
+package a
+
+func DoSomething(int) {}
+
+func _() {
+ DoSomething()
+}
+-- b/go.mod --
+module b.com
+go 1.18
+
+require a.com v1.0.0
+
+replace a.com => ../a
+-- b/b/b.go --
+package b
+
+import "a.com/a"
+
+func _() {
+ a.DoSomething()
+}
+`
+
+ WithOptions(
+ WorkspaceFolders("a"),
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a/a.go")
+ env.OpenFile("b/b/b.go")
+ signatureHelp := func(filename string) *protocol.SignatureHelp {
+ loc := env.RegexpSearch(filename, `DoSomething\(()\)`)
+ var params protocol.SignatureHelpParams
+ params.TextDocument.URI = loc.URI
+ params.Position = loc.Range.Start
+ help, err := env.Editor.Server.SignatureHelp(env.Ctx, &params)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return help
+ }
+ ahelp := signatureHelp("a/a/a.go")
+ bhelp := signatureHelp("b/b/b.go")
+
+ if diff := cmp.Diff(ahelp, bhelp); diff != "" {
+ t.Fatal(diff)
+ }
+ })
+}
diff --git a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go
new file mode 100644
index 000000000..fa049ab0e
--- /dev/null
+++ b/gopls/internal/regtest/misc/staticcheck_test.go
@@ -0,0 +1,110 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package misc
+
+import (
+ "testing"
+
+ "golang.org/x/tools/internal/testenv"
+
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+)
+
+func TestStaticcheckGenerics(t *testing.T) {
+ testenv.NeedsGo1Point(t, 19) // generics were introduced in Go 1.18, staticcheck requires go1.19+
+
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- a/a.go --
+package a
+
+import (
+ "errors"
+ "sort"
+ "strings"
+)
+
+func Zero[P any]() P {
+ var p P
+ return p
+}
+
+type Inst[P any] struct {
+ Field P
+}
+
+func testGenerics[P *T, T any](p P) {
+ // Calls to instantiated functions should not break checks.
+ slice := Zero[string]()
+ sort.Slice(slice, func(i, j int) bool {
+ return slice[i] < slice[j]
+ })
+
+ // Usage of instantiated fields should not break checks.
+ g := Inst[string]{"hello"}
+ g.Field = strings.TrimLeft(g.Field, "12234")
+
+ // Use of type parameters should not break checks.
+ var q P
+ p = q // SA4009: p is overwritten before its first use
+ q = &*p // SA4001: &* will be simplified
+}
+
+
+// FooErr should be called ErrFoo (ST1012)
+var FooErr error = errors.New("foo")
+`
+
+ WithOptions(
+ Settings{"staticcheck": true},
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("a/a.go", "sort.Slice"), FromSource("sortslice")),
+ Diagnostics(env.AtRegexp("a/a.go", "sort.Slice.(slice)"), FromSource("SA1028")),
+ Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)"), FromSource("ST1012")),
+ Diagnostics(env.AtRegexp("a/a.go", `"12234"`), FromSource("SA1024")),
+ Diagnostics(env.AtRegexp("a/a.go", "testGenerics.*(p P)"), FromSource("SA4009")),
+ Diagnostics(env.AtRegexp("a/a.go", "q = (&\\*p)"), FromSource("SA4001")),
+ )
+ })
+}
+
+// Test for golang/go#56270: an analysis with related info should not panic if
+// analysis.RelatedInformation.End is not set.
+func TestStaticcheckRelatedInfo(t *testing.T) {
+ testenv.NeedsGo1Point(t, 19) // staticcheck is only supported at Go 1.19+
+ const files = `
+-- go.mod --
+module mod.test
+
+go 1.18
+-- p.go --
+package p
+
+import (
+ "fmt"
+)
+
+func Foo(enabled interface{}) {
+ if enabled, ok := enabled.(bool); ok {
+ } else {
+ _ = fmt.Sprintf("invalid type %T", enabled) // enabled is always bool here
+ }
+}
+`
+
+ WithOptions(
+ Settings{"staticcheck": true},
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("p.go")
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("p.go", ", (enabled)"), FromSource("SA9008")),
+ )
+ })
+}
diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go
index 0e615f281..4fcf1067a 100644
--- a/gopls/internal/regtest/misc/vendor_test.go
+++ b/gopls/internal/regtest/misc/vendor_test.go
@@ -5,13 +5,11 @@
package misc
import (
- "runtime"
"testing"
- . "golang.org/x/tools/internal/lsp/regtest"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/testenv"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
)
const basicProxy = `
@@ -26,11 +24,6 @@ var Goodbye error
`
func TestInconsistentVendoring(t *testing.T) {
- testenv.NeedsGo1Point(t, 14)
- if runtime.GOOS == "windows" {
- t.Skipf("skipping test due to flakiness on Windows: https://golang.org/issue/49646")
- }
-
const pkgThatUsesVendoring = `
-- go.mod --
module mod.com
@@ -53,21 +46,20 @@ func _() {
}
`
WithOptions(
- Modes(Singleton),
+ Modes(Default),
ProxyFiles(basicProxy),
).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) {
env.OpenFile("a/a1.go")
d := &protocol.PublishDiagnosticsParams{}
- env.Await(
- OnceMet(
- env.DiagnosticAtRegexpWithMessage("go.mod", "module mod.com", "Inconsistent vendoring"),
- ReadDiagnostics("go.mod", d),
- ),
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ Diagnostics(env.AtRegexp("go.mod", "module mod.com"), WithMessage("Inconsistent vendoring")),
+ ReadDiagnostics("go.mod", d),
)
env.ApplyQuickFixes("go.mod", d.Diagnostics)
- env.Await(
- env.DiagnosticAtRegexpWithMessage("a/a1.go", `q int`, "not used"),
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("a/a1.go", `q int`), WithMessage("not used")),
)
})
}
diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go
index 94fde715c..8badc879e 100644
--- a/gopls/internal/regtest/misc/vuln_test.go
+++ b/gopls/internal/regtest/misc/vuln_test.go
@@ -2,17 +2,32 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+//go:build go1.18
+// +build go1.18
+
package misc
import (
+ "context"
+ "encoding/json"
+ "path/filepath"
+ "sort"
+ "strings"
"testing"
- "golang.org/x/tools/internal/lsp/command"
- "golang.org/x/tools/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/govulncheck"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/lsp/tests/compare"
+ "golang.org/x/tools/gopls/internal/vulncheck"
+ "golang.org/x/tools/gopls/internal/vulncheck/vulntest"
+ "golang.org/x/tools/internal/testenv"
)
-func TestRunVulncheckExpError(t *testing.T) {
+func TestRunGovulncheckError(t *testing.T) {
const files = `
-- go.mod --
module mod.com
@@ -22,15 +37,15 @@ go 1.12
package foo
`
Run(t, files, func(t *testing.T, env *Env) {
- cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{
- Dir: "/invalid/file/url", // invalid arg
+ cmd, err := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{
+ URI: "/invalid/file/url", // invalid arg
})
if err != nil {
t.Fatal(err)
}
params := &protocol.ExecuteCommandParams{
- Command: command.RunVulncheckExp.ID(),
+ Command: command.RunGovulncheck.ID(),
Arguments: cmd.Arguments,
}
@@ -41,3 +56,922 @@ package foo
}
})
}
+
+func TestRunGovulncheckError2(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- foo.go --
+package foo
+
+func F() { // build error incomplete
+`
+ WithOptions(
+ EnvVars{
+ "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
+ },
+ Settings{
+ "codelenses": map[string]bool{
+ "run_govulncheck": true,
+ },
+ },
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("go.mod")
+ var result command.RunVulncheckResult
+ env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
+ var ws WorkStatus
+ env.Await(
+ CompletedProgress(result.Token, &ws),
+ )
+ wantEndMsg, wantMsgPart := "failed", "failed to load packages due to errors"
+ if ws.EndMsg != "failed" || !strings.Contains(ws.Msg, wantMsgPart) {
+ t.Errorf("work status = %+v, want {EndMessage: %q, Message: %q}", ws, wantEndMsg, wantMsgPart)
+ }
+ })
+}
+
+const vulnsData = `
+-- GO-2022-01.yaml --
+modules:
+ - module: golang.org/amod
+ versions:
+ - introduced: 1.0.0
+ - fixed: 1.0.4
+ - introduced: 1.1.2
+ packages:
+ - package: golang.org/amod/avuln
+ symbols:
+ - VulnData.Vuln1
+ - VulnData.Vuln2
+description: >
+ vuln in amod
+references:
+ - href: pkg.go.dev/vuln/GO-2022-01
+-- GO-2022-03.yaml --
+modules:
+ - module: golang.org/amod
+ versions:
+ - introduced: 1.0.0
+ - fixed: 1.0.6
+ packages:
+ - package: golang.org/amod/avuln
+ symbols:
+ - nonExisting
+description: >
+ unaffecting vulnerability
+-- GO-2022-02.yaml --
+modules:
+ - module: golang.org/bmod
+ packages:
+ - package: golang.org/bmod/bvuln
+ symbols:
+ - Vuln
+description: |
+ vuln in bmod
+
+ This is a long description
+ of this vulnerability.
+references:
+ - href: pkg.go.dev/vuln/GO-2022-03
+-- GO-2022-04.yaml --
+modules:
+ - module: golang.org/bmod
+ packages:
+ - package: golang.org/bmod/unused
+ symbols:
+ - Vuln
+description: |
+ vuln in bmod/somtrhingelse
+references:
+ - href: pkg.go.dev/vuln/GO-2022-04
+-- GOSTDLIB.yaml --
+modules:
+ - module: stdlib
+ versions:
+ - introduced: 1.18.0
+ packages:
+ - package: archive/zip
+ symbols:
+ - OpenReader
+references:
+ - href: pkg.go.dev/vuln/GOSTDLIB
+`
+
+func TestRunGovulncheckStd(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- main.go --
+package main
+
+import (
+ "archive/zip"
+ "fmt"
+)
+
+func main() {
+ _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB
+ fmt.Println(err)
+}
+`
+
+ db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Clean()
+ WithOptions(
+ EnvVars{
+ // Let the analyzer read vulnerabilities data from the testdata/vulndb.
+ "GOVULNDB": db.URI(),
+ // When fetchinging stdlib package vulnerability info,
+ // behave as if our go version is go1.18 for this testing.
+ // The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
+ vulncheck.GoVersionForVulnTest: "go1.18",
+ "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
+ },
+ Settings{
+ "codelenses": map[string]bool{
+ "run_govulncheck": true,
+ },
+ },
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("go.mod")
+
+ // Test CodeLens is present.
+ lenses := env.CodeLens("go.mod")
+
+ const wantCommand = "gopls." + string(command.RunGovulncheck)
+ var gotCodelens = false
+ var lens protocol.CodeLens
+ for _, l := range lenses {
+ if l.Command.Command == wantCommand {
+ gotCodelens = true
+ lens = l
+ break
+ }
+ }
+ if !gotCodelens {
+ t.Fatal("got no vulncheck codelens")
+ }
+ // Run Command included in the codelens.
+ var result command.RunVulncheckResult
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: lens.Command.Command,
+ Arguments: lens.Command.Arguments,
+ }, &result)
+
+ env.OnceMet(
+ CompletedProgress(result.Token, nil),
+ ShownMessage("Found GOSTDLIB"),
+ NoDiagnostics(ForFile("go.mod")),
+ )
+ testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
+ "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: govulncheck.ModeGovulncheck}})
+ })
+}
+
+func TestFetchVulncheckResultStd(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.18
+-- main.go --
+package main
+
+import (
+ "archive/zip"
+ "fmt"
+)
+
+func main() {
+ _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB
+ fmt.Println(err)
+}
+`
+
+ db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Clean()
+ WithOptions(
+ EnvVars{
+ // Let the analyzer read vulnerabilities data from the testdata/vulndb.
+ "GOVULNDB": db.URI(),
+ // When fetchinging stdlib package vulnerability info,
+ // behave as if our go version is go1.18 for this testing.
+ vulncheck.GoVersionForVulnTest: "go1.18",
+ "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
+ },
+ Settings{"ui.diagnostic.vulncheck": "Imports"},
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("go.mod")
+ env.AfterChange(
+ NoDiagnostics(ForFile("go.mod")),
+ // we don't publish diagnostics for standard library vulnerability yet.
+ )
+ testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
+ "go.mod": {
+ IDs: []string{"GOSTDLIB"},
+ Mode: govulncheck.ModeImports,
+ },
+ })
+ })
+}
+
+type fetchVulncheckResult struct {
+ IDs []string
+ Mode govulncheck.AnalysisMode
+}
+
+func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulncheckResult) {
+ t.Helper()
+
+ var result map[protocol.DocumentURI]*govulncheck.Result
+ fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{
+ URI: env.Sandbox.Workdir.URI("go.mod"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: fetchCmd.Command,
+ Arguments: fetchCmd.Arguments,
+ }, &result)
+
+ for _, v := range want {
+ sort.Strings(v.IDs)
+ }
+ got := map[string]fetchVulncheckResult{}
+ for k, r := range result {
+ var osv []string
+ for _, v := range r.Vulns {
+ osv = append(osv, v.OSV.ID)
+ }
+ sort.Strings(osv)
+ modfile := env.Sandbox.Workdir.RelPath(k.SpanURI().Filename())
+ got[modfile] = fetchVulncheckResult{
+ IDs: osv,
+ Mode: r.Mode,
+ }
+ }
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff)
+ }
+}
+
+const workspace1 = `
+-- go.mod --
+module golang.org/entry
+
+go 1.18
+
+require golang.org/cmod v1.1.3
+
+require (
+ golang.org/amod v1.0.0 // indirect
+ golang.org/bmod v0.5.0 // indirect
+)
+-- go.sum --
+golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k=
+golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E=
+golang.org/bmod v0.5.0 h1:KgvUulMyMiYRB7suKA0x+DfWRVdeyPgVJvcishTH+ng=
+golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c=
+golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ=
+golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA=
+-- x/x.go --
+package x
+
+import (
+ "golang.org/cmod/c"
+ "golang.org/entry/y"
+)
+
+func X() {
+ c.C1().Vuln1() // vuln use: X -> Vuln1
+}
+
+func CallY() {
+ y.Y() // vuln use: CallY -> y.Y -> bvuln.Vuln
+}
+
+-- y/y.go --
+package y
+
+import "golang.org/cmod/c"
+
+func Y() {
+ c.C2()() // vuln use: Y -> bvuln.Vuln
+}
+`
+
+// cmod/c imports amod/avuln and bmod/bvuln.
+const proxy1 = `
+-- golang.org/cmod@v1.1.3/go.mod --
+module golang.org/cmod
+
+go 1.12
+-- golang.org/cmod@v1.1.3/c/c.go --
+package c
+
+import (
+ "golang.org/amod/avuln"
+ "golang.org/bmod/bvuln"
+)
+
+type I interface {
+ Vuln1()
+}
+
+func C1() I {
+ v := avuln.VulnData{}
+ v.Vuln2() // vuln use
+ return v
+}
+
+func C2() func() {
+ return bvuln.Vuln
+}
+-- golang.org/amod@v1.0.0/go.mod --
+module golang.org/amod
+
+go 1.14
+-- golang.org/amod@v1.0.0/avuln/avuln.go --
+package avuln
+
+type VulnData struct {}
+func (v VulnData) Vuln1() {}
+func (v VulnData) Vuln2() {}
+-- golang.org/amod@v1.0.4/go.mod --
+module golang.org/amod
+
+go 1.14
+-- golang.org/amod@v1.0.4/avuln/avuln.go --
+package avuln
+
+type VulnData struct {}
+func (v VulnData) Vuln1() {}
+func (v VulnData) Vuln2() {}
+
+-- golang.org/bmod@v0.5.0/go.mod --
+module golang.org/bmod
+
+go 1.14
+-- golang.org/bmod@v0.5.0/bvuln/bvuln.go --
+package bvuln
+
+func Vuln() {
+ // something evil
+}
+-- golang.org/bmod@v0.5.0/unused/unused.go --
+package unused
+
+func Vuln() {
+ // something evil
+}
+-- golang.org/amod@v1.0.6/go.mod --
+module golang.org/amod
+
+go 1.14
+-- golang.org/amod@v1.0.6/avuln/avuln.go --
+package avuln
+
+type VulnData struct {}
+func (v VulnData) Vuln1() {}
+func (v VulnData) Vuln2() {}
+`
+
+func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) {
+ db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
+ if err != nil {
+ return nil, nil, nil
+ }
+ settings := Settings{
+ "codelenses": map[string]bool{
+ "run_govulncheck": true,
+ },
+ }
+ ev := EnvVars{
+ // Let the analyzer read vulnerabilities data from the testdata/vulndb.
+ "GOVULNDB": db.URI(),
+ // When fetching stdlib package vulnerability info,
+ // behave as if our go version is go1.18 for this testing.
+ // The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
+ vulncheck.GoVersionForVulnTest: "go1.18",
+ "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
+ "GOSUMDB": "off",
+ }
+ return db, []RunOption{ProxyFiles(proxyData), ev, settings}, nil
+}
+
+func TestRunVulncheckPackageDiagnostics(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+
+ db, opts0, err := vulnTestEnv(vulnsData, proxy1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Clean()
+
+ checkVulncheckDiagnostics := func(env *Env, t *testing.T) {
+ env.OpenFile("go.mod")
+
+ gotDiagnostics := &protocol.PublishDiagnosticsParams{}
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
+ ReadDiagnostics("go.mod", gotDiagnostics),
+ )
+
+ testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
+ "go.mod": {
+ IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"},
+ Mode: govulncheck.ModeImports,
+ },
+ })
+
+ wantVulncheckDiagnostics := map[string]vulnDiagExpectation{
+ "golang.org/amod": {
+ diagnostics: []vulnDiag{
+ {
+ msg: "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.",
+ severity: protocol.SeverityInformation,
+ source: string(source.Vulncheck),
+ codeActions: []string{
+ "Run govulncheck to verify",
+ "Upgrade to v1.0.6",
+ "Upgrade to latest",
+ },
+ },
+ },
+ codeActions: []string{
+ "Run govulncheck to verify",
+ "Upgrade to v1.0.6",
+ "Upgrade to latest",
+ },
+ hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"},
+ },
+ "golang.org/bmod": {
+ diagnostics: []vulnDiag{
+ {
+ msg: "golang.org/bmod has a vulnerability GO-2022-02.",
+ severity: protocol.SeverityInformation,
+ source: string(source.Vulncheck),
+ codeActions: []string{
+ "Run govulncheck to verify",
+ },
+ },
+ },
+ codeActions: []string{
+ "Run govulncheck to verify",
+ },
+ hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
+ },
+ }
+
+ for pattern, want := range wantVulncheckDiagnostics {
+ modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics)
+
+ gotActions := env.CodeAction("go.mod", modPathDiagnostics)
+ if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
+ t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff)
+ continue
+ }
+ }
+ }
+
+ wantNoVulncheckDiagnostics := func(env *Env, t *testing.T) {
+ env.OpenFile("go.mod")
+
+ gotDiagnostics := &protocol.PublishDiagnosticsParams{}
+ env.AfterChange(
+ ReadDiagnostics("go.mod", gotDiagnostics),
+ )
+
+ if len(gotDiagnostics.Diagnostics) > 0 {
+ t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics))
+ }
+ testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{})
+ }
+
+ for _, tc := range []struct {
+ name string
+ setting Settings
+ wantDiagnostics bool
+ }{
+ {"imports", Settings{"ui.diagnostic.vulncheck": "Imports"}, true},
+ {"default", Settings{}, false},
+ {"invalid", Settings{"ui.diagnostic.vulncheck": "invalid"}, false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ // override the settings options to enable diagnostics
+ opts := append(opts0, tc.setting)
+ WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) {
+ // TODO(hyangah): implement it, so we see GO-2022-01, GO-2022-02, and GO-2022-03.
+ // Check that the actions we get when including all diagnostics at a location return the same result
+ if tc.wantDiagnostics {
+ checkVulncheckDiagnostics(env, t)
+ } else {
+ wantNoVulncheckDiagnostics(env, t)
+ }
+
+ if tc.name == "imports" && tc.wantDiagnostics {
+ // test we get only govulncheck-based diagnostics after "run govulncheck".
+ var result command.RunVulncheckResult
+ env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
+ gotDiagnostics := &protocol.PublishDiagnosticsParams{}
+ env.OnceMet(
+ CompletedProgress(result.Token, nil),
+ ShownMessage("Found"),
+ )
+ env.OnceMet(
+ Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
+ ReadDiagnostics("go.mod", gotDiagnostics),
+ )
+ // We expect only one diagnostic for GO-2022-02.
+ count := 0
+ for _, diag := range gotDiagnostics.Diagnostics {
+ if strings.Contains(diag.Message, "GO-2022-02") {
+ count++
+ if got, want := diag.Severity, protocol.SeverityWarning; got != want {
+ t.Errorf("Diagnostic for GO-2022-02 = %v, want %v", got, want)
+ }
+ }
+ }
+ if count != 1 {
+ t.Errorf("Unexpected number of diagnostics about GO-2022-02 = %v, want 1:\n%+v", count, stringify(gotDiagnostics))
+ }
+ }
+ })
+ })
+ }
+}
+
+func stringify(a interface{}) string {
+ data, _ := json.Marshal(a)
+ return string(data)
+}
+
+func TestRunVulncheckWarning(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+
+ db, opts, err := vulnTestEnv(vulnsData, proxy1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Clean()
+ WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) {
+ env.OpenFile("go.mod")
+
+ var result command.RunVulncheckResult
+ env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
+ gotDiagnostics := &protocol.PublishDiagnosticsParams{}
+ env.OnceMet(
+ CompletedProgress(result.Token, nil),
+ ShownMessage("Found"),
+ )
+ // Vulncheck diagnostics asynchronous to the vulncheck command.
+ env.OnceMet(
+ Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
+ ReadDiagnostics("go.mod", gotDiagnostics),
+ )
+
+ testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
+ "go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: govulncheck.ModeGovulncheck},
+ })
+ env.OpenFile("x/x.go")
+ lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`).Range.Start
+ env.OpenFile("y/y.go")
+ lineY := env.RegexpSearch("y/y.go", `c\.C2\(\)\(\)`).Range.Start
+ wantDiagnostics := map[string]vulnDiagExpectation{
+ "golang.org/amod": {
+ applyAction: "Upgrade to v1.0.6",
+ diagnostics: []vulnDiag{
+ {
+ msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.",
+ severity: protocol.SeverityWarning,
+ source: string(source.Govulncheck),
+ codeActions: []string{
+ "Upgrade to v1.0.4",
+ "Upgrade to latest",
+ "Reset govulncheck result",
+ },
+ relatedInfo: []vulnRelatedInfo{
+ {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1
+ {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2
+ },
+ },
+ {
+ msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.",
+ severity: protocol.SeverityInformation,
+ source: string(source.Govulncheck),
+ codeActions: []string{
+ "Upgrade to v1.0.6",
+ "Upgrade to latest",
+ "Reset govulncheck result",
+ },
+ relatedInfo: []vulnRelatedInfo{
+ {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1
+ {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2
+ },
+ },
+ },
+ codeActions: []string{
+ "Upgrade to v1.0.6",
+ "Upgrade to latest",
+ "Reset govulncheck result",
+ },
+ hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"},
+ },
+ "golang.org/bmod": {
+ diagnostics: []vulnDiag{
+ {
+ msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.",
+ severity: protocol.SeverityWarning,
+ source: string(source.Govulncheck),
+ codeActions: []string{
+ "Reset govulncheck result", // no fix, but we should give an option to reset.
+ },
+ relatedInfo: []vulnRelatedInfo{
+ {"y.go", uint32(lineY.Line), "[GO-2022-02]"}, // bvuln.Vuln
+ },
+ },
+ },
+ codeActions: []string{
+ "Reset govulncheck result", // no fix, but we should give an option to reset.
+ },
+ hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
+ },
+ }
+
+ for mod, want := range wantDiagnostics {
+ modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics)
+
+ // Check that the actions we get when including all diagnostics at a location return the same result
+ gotActions := env.CodeAction("go.mod", modPathDiagnostics)
+ if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
+ t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff)
+ continue
+ }
+
+ // Apply the code action matching applyAction.
+ if want.applyAction == "" {
+ continue
+ }
+ for _, action := range gotActions {
+ if action.Title == want.applyAction {
+ env.ApplyCodeAction(action)
+ break
+ }
+ }
+ }
+
+ env.Await(env.DoneWithChangeWatchedFiles())
+ wantGoMod := `module golang.org/entry
+
+go 1.18
+
+require golang.org/cmod v1.1.3
+
+require (
+ golang.org/amod v1.0.6 // indirect
+ golang.org/bmod v0.5.0 // indirect
+)
+`
+ if got := env.BufferText("go.mod"); got != wantGoMod {
+ t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got))
+ }
+ })
+}
+
+func diffCodeActions(gotActions []protocol.CodeAction, want []string) string {
+ var gotTitles []string
+ for _, ca := range gotActions {
+ gotTitles = append(gotTitles, ca.Title)
+ }
+ return cmp.Diff(want, gotTitles)
+}
+
+const workspace2 = `
+-- go.mod --
+module golang.org/entry
+
+go 1.18
+
+require golang.org/bmod v0.5.0
+
+-- go.sum --
+golang.org/bmod v0.5.0 h1:MT/ysNRGbCiURc5qThRFWaZ5+rK3pQRPo9w7dYZfMDk=
+golang.org/bmod v0.5.0/go.mod h1:k+zl+Ucu4yLIjndMIuWzD/MnOHy06wqr3rD++y0abVs=
+-- x/x.go --
+package x
+
+import "golang.org/bmod/bvuln"
+
+func F() {
+ // Calls a benign func in bvuln.
+ bvuln.OK()
+}
+`
+
+const proxy2 = `
+-- golang.org/bmod@v0.5.0/bvuln/bvuln.go --
+package bvuln
+
+func Vuln() {} // vulnerable.
+func OK() {} // ok.
+`
+
+func TestGovulncheckInfo(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18)
+
+ db, opts, err := vulnTestEnv(vulnsData, proxy2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Clean()
+ WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) {
+ env.OpenFile("go.mod")
+ var result command.RunVulncheckResult
+ env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
+ gotDiagnostics := &protocol.PublishDiagnosticsParams{}
+ env.OnceMet(
+ CompletedProgress(result.Token, nil),
+ ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities.
+ )
+
+ // Vulncheck diagnostics asynchronous to the vulncheck command.
+ env.OnceMet(
+ Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
+ ReadDiagnostics("go.mod", gotDiagnostics),
+ )
+
+ testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: govulncheck.ModeGovulncheck}})
+ // wantDiagnostics maps a module path in the require
+ // section of a go.mod to diagnostics that will be returned
+ // when running vulncheck.
+ wantDiagnostics := map[string]vulnDiagExpectation{
+ "golang.org/bmod": {
+ diagnostics: []vulnDiag{
+ {
+ msg: "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.",
+ severity: protocol.SeverityInformation,
+ source: string(source.Govulncheck),
+ codeActions: []string{
+ "Reset govulncheck result",
+ },
+ },
+ },
+ codeActions: []string{
+ "Reset govulncheck result",
+ },
+ hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
+ },
+ }
+
+ var allActions []protocol.CodeAction
+ for mod, want := range wantDiagnostics {
+ modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics)
+ // Check that the actions we get when including all diagnostics at a location return the same result
+ gotActions := env.CodeAction("go.mod", modPathDiagnostics)
+ allActions = append(allActions, gotActions...)
+ if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
+ t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff)
+ continue
+ }
+ }
+
+ // Clear Diagnostics by using one of the reset code actions.
+ var reset protocol.CodeAction
+ for _, a := range allActions {
+ if a.Title == "Reset govulncheck result" {
+ reset = a
+ break
+ }
+ }
+ if reset.Title != "Reset govulncheck result" {
+ t.Errorf("failed to find a 'Reset govulncheck result' code action, got %v", allActions)
+ }
+ env.ApplyCodeAction(reset)
+
+ env.Await(NoDiagnostics(ForFile("go.mod")))
+ })
+}
+
+// testVulnDiagnostics finds the require or module statement line for the requireMod in go.mod file
+// and runs checks if diagnostics and code actions associated with the line match expectation.
+func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic {
+ t.Helper()
+ loc := env.RegexpSearch("go.mod", pattern)
+ var modPathDiagnostics []protocol.Diagnostic
+ for _, w := range want.diagnostics {
+ // Find the diagnostics at loc.start.
+ var diag *protocol.Diagnostic
+ for _, g := range got.Diagnostics {
+ g := g
+ if g.Range.Start == loc.Range.Start && w.msg == g.Message {
+ modPathDiagnostics = append(modPathDiagnostics, g)
+ diag = &g
+ break
+ }
+ }
+ if diag == nil {
+ t.Errorf("no diagnostic at %q matching %q found\n", pattern, w.msg)
+ continue
+ }
+ if diag.Severity != w.severity || diag.Source != w.source {
+ t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source)
+ }
+ sort.Slice(w.relatedInfo, func(i, j int) bool { return w.relatedInfo[i].less(w.relatedInfo[j]) })
+ if got, want := summarizeRelatedInfo(diag.RelatedInformation), w.relatedInfo; !cmp.Equal(got, want) {
+ t.Errorf("related info for %q do not match, want %v, got %v\n", w.msg, want, got)
+ }
+ // Check expected code actions appear.
+ gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag})
+ if diff := diffCodeActions(gotActions, w.codeActions); diff != "" {
+ t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff)
+ continue
+ }
+ }
+ // Check that useful info is supplemented as hover.
+ if len(want.hover) > 0 {
+ hover, _ := env.Hover(loc)
+ for _, part := range want.hover {
+ if !strings.Contains(hover.Value, part) {
+ t.Errorf("hover contents for %q do not match, want %v, got %v\n", pattern, strings.Join(want.hover, ","), hover.Value)
+ break
+ }
+ }
+ }
+ return modPathDiagnostics
+}
+
+// summarizeRelatedInfo converts protocol.DiagnosticRelatedInformation to vulnRelatedInfo
+// that captures only the part that we want to test.
+func summarizeRelatedInfo(rinfo []protocol.DiagnosticRelatedInformation) []vulnRelatedInfo {
+ var res []vulnRelatedInfo
+ for _, r := range rinfo {
+ filename := filepath.Base(r.Location.URI.SpanURI().Filename())
+ message, _, _ := strings.Cut(r.Message, " ")
+ line := r.Location.Range.Start.Line
+ res = append(res, vulnRelatedInfo{filename, line, message})
+ }
+ sort.Slice(res, func(i, j int) bool {
+ return res[i].less(res[j])
+ })
+ return res
+}
+
+type vulnRelatedInfo struct {
+ Filename string
+ Line uint32
+ Message string
+}
+
+type vulnDiag struct {
+ msg string
+ severity protocol.DiagnosticSeverity
+ // codeActions is a list titles of code actions that we get with this
+ // diagnostics as the context.
+ codeActions []string
+ // relatedInfo is related info message prefixed by the file base.
+ // See summarizeRelatedInfo.
+ relatedInfo []vulnRelatedInfo
+ // diagnostic source.
+ source string
+}
+
+func (i vulnRelatedInfo) less(j vulnRelatedInfo) bool {
+ if i.Filename != j.Filename {
+ return i.Filename < j.Filename
+ }
+ if i.Line != j.Line {
+ return i.Line < j.Line
+ }
+ return i.Message < j.Message
+}
+
+// vulnDiagExpectation maps a module path in the require
+// section of a go.mod to diagnostics that will be returned
+// when running vulncheck.
+type vulnDiagExpectation struct {
+ // applyAction is the title of the code action to run for this module.
+ // If empty, no code actions will be executed.
+ applyAction string
+ // diagnostics is the list of diagnostics we expect at the require line for
+ // the module path.
+ diagnostics []vulnDiag
+ // codeActions is a list titles of code actions that we get with context
+ // diagnostics.
+ codeActions []string
+ // hover message is the list of expected hover message parts for this go.mod require line.
+ // all parts must appear in the hover message.
+ hover []string
+}
diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go
index a21d47312..a492e1d49 100644
--- a/gopls/internal/regtest/misc/workspace_symbol_test.go
+++ b/gopls/internal/regtest/misc/workspace_symbol_test.go
@@ -7,16 +7,12 @@ package misc
import (
"testing"
- "golang.org/x/tools/internal/lsp/protocol"
- . "golang.org/x/tools/internal/lsp/regtest"
- "golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/testenv"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ . "golang.org/x/tools/gopls/internal/lsp/regtest"
+ "golang.org/x/tools/gopls/internal/lsp/source"
)
func TestWorkspaceSymbolMissingMetadata(t *testing.T) {
- // We get 2 symbols on 1.12, for some reason.
- testenv.NeedsGo1Point(t, 13)
-
const files = `
-- go.mod --
module mod.com
@@ -26,26 +22,27 @@ go 1.17
package p
const C1 = "a.go"
--- ignore.go --
+-- exclude.go --
-// +build ignore
+//go:build exclude
+// +build exclude
-package ignore
+package exclude
-const C2 = "ignore.go"
+const C2 = "exclude.go"
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
- syms := env.WorkspaceSymbol("C")
+ syms := env.Symbol("C")
if got, want := len(syms), 1; got != want {
t.Errorf("got %d symbols, want %d", got, want)
}
// Opening up an ignored file will result in an overlay with missing
// metadata, but this shouldn't break workspace symbols requests.
- env.OpenFile("ignore.go")
- syms = env.WorkspaceSymbol("C")
+ env.OpenFile("exclude.go")
+ syms = env.Symbol("C")
if got, want := len(syms), 1; got != want {
t.Errorf("got %d symbols, want %d", got, want)
}
@@ -72,9 +69,7 @@ const (
var symbolMatcher = string(source.SymbolFastFuzzy)
WithOptions(
- EditorConfig{
- SymbolMatcher: &symbolMatcher,
- },
+ Settings{"symbolMatcher": symbolMatcher},
).Run(t, files, func(t *testing.T, env *Env) {
want := []string{
"Foo", // prefer exact segment matches first
@@ -83,8 +78,8 @@ const (
"Fooey", // shorter than Fooest, Foobar
"Fooest",
}
- got := env.WorkspaceSymbol("Foo")
- compareSymbols(t, got, want)
+ got := env.Symbol("Foo")
+ compareSymbols(t, got, want...)
})
}
@@ -105,19 +100,17 @@ const (
var symbolMatcher = string(source.SymbolFastFuzzy)
WithOptions(
- EditorConfig{
- SymbolMatcher: &symbolMatcher,
- },
+ Settings{"symbolMatcher": symbolMatcher},
).Run(t, files, func(t *testing.T, env *Env) {
- compareSymbols(t, env.WorkspaceSymbol("ABC"), []string{"ABC", "AxxBxxCxx"})
- compareSymbols(t, env.WorkspaceSymbol("'ABC"), []string{"ABC"})
- compareSymbols(t, env.WorkspaceSymbol("^mod.com"), []string{"mod.com/a.ABC", "mod.com/a.AxxBxxCxx"})
- compareSymbols(t, env.WorkspaceSymbol("^mod.com Axx"), []string{"mod.com/a.AxxBxxCxx"})
- compareSymbols(t, env.WorkspaceSymbol("C$"), []string{"ABC"})
+ compareSymbols(t, env.Symbol("ABC"), "ABC", "AxxBxxCxx")
+ compareSymbols(t, env.Symbol("'ABC"), "ABC")
+ compareSymbols(t, env.Symbol("^mod.com"), "mod.com/a.ABC", "mod.com/a.AxxBxxCxx")
+ compareSymbols(t, env.Symbol("^mod.com Axx"), "mod.com/a.AxxBxxCxx")
+ compareSymbols(t, env.Symbol("C$"), "ABC")
})
}
-func compareSymbols(t *testing.T, got []protocol.SymbolInformation, want []string) {
+func compareSymbols(t *testing.T, got []protocol.SymbolInformation, want ...string) {
t.Helper()
if len(got) != len(want) {
t.Errorf("got %d symbols, want %d", len(got), len(want))