diff options
Diffstat (limited to 'gopls/internal/regtest/misc')
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, ¶ms) 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, ¶ms) + if err != nil { + t.Fatal(err) + } + return help + } + ahelp := signatureHelp("a/a/a.go") + bhelp := signatureHelp("b/b/b.go") + + if diff := cmp.Diff(ahelp, bhelp); diff != "" { + t.Fatal(diff) + } + }) +} 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)) |