| // Copyright 2021 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 ( | |
| "fmt" | |
| "regexp" | |
| "strings" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "golang.org/x/tools/gopls/internal/protocol" | |
| . "golang.org/x/tools/gopls/internal/test/integration" | |
| "golang.org/x/tools/gopls/internal/test/integration/fake" | |
| ) | |
| func TestHoverUnexported(t *testing.T) { | |
| const proxy = ` | |
| -- golang.org/x/structs@v1.0.0/go.mod -- | |
| module golang.org/x/structs | |
| go 1.21 | |
| -- golang.org/x/structs@v1.0.0/types.go -- | |
| package structs | |
| type Mixed struct { | |
| // Exported comment | |
| Exported int | |
| unexported string | |
| } | |
| func printMixed(m Mixed) { | |
| println(m) | |
| } | |
| ` | |
| const mod = ` | |
| -- go.mod -- | |
| module mod.com | |
| go 1.21 | |
| require golang.org/x/structs v1.0.0 | |
| -- main.go -- | |
| package main | |
| import "golang.org/x/structs" | |
| func main() { | |
| var m structs.Mixed | |
| _ = m.Exported | |
| } | |
| ` | |
| // TODO: use a nested workspace folder here. | |
| WithOptions( | |
| ProxyFiles(proxy), | |
| WriteGoSum("."), | |
| ).Run(t, mod, func(t *testing.T, env *Env) { | |
| env.OpenFile("main.go") | |
| 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) | |
| } | |
| cacheLoc := env.FirstDefinition(mixedLoc) | |
| cacheFile := env.Sandbox.Workdir.URIToPath(cacheLoc.URI) | |
| env.OpenFile(cacheFile) | |
| 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) | |
| } | |
| 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) | |
| } | |
| }) | |
| } | |
| func TestHoverIntLiteral(t *testing.T) { | |
| const source = ` | |
| -- main.go -- | |
| package main | |
| var ( | |
| bigBin = 0b1001001 | |
| ) | |
| var hex = 0xe34e | |
| func main() { | |
| } | |
| ` | |
| Run(t, source, func(t *testing.T, env *Env) { | |
| env.OpenFile("main.go") | |
| hexExpected := "58190" | |
| got, _ := env.Hover(env.RegexpSearch("main.go", "0xe")) | |
| if got != nil && !strings.Contains(got.Value, hexExpected) { | |
| t.Errorf("Hover: missing expected field '%s'. Got:\n%q", hexExpected, got.Value) | |
| } | |
| binExpected := "73" | |
| got, _ = env.Hover(env.RegexpSearch("main.go", "0b1")) | |
| if got != nil && !strings.Contains(got.Value, binExpected) { | |
| t.Errorf("Hover: missing expected field '%s'. Got:\n%q", binExpected, got.Value) | |
| } | |
| }) | |
| } | |
| // Tests that hovering does not trigger the panic in golang/go#48249. | |
| func TestPanicInHoverBrokenCode(t *testing.T) { | |
| // 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 | |
| type Example struct` | |
| Run(t, source, func(t *testing.T, env *Env) { | |
| env.OpenFile("main.go") | |
| env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "Example")) | |
| }) | |
| } | |
| func TestHoverRune_48492(t *testing.T) { | |
| const files = ` | |
| -- go.mod -- | |
| module mod.com | |
| go 1.18 | |
| -- main.go -- | |
| 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, 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) | |
| } | |
| } | |
| }) | |
| } | |
| func TestHoverPackageIdent(t *testing.T) { | |
| const packageDoc1 = "Package lib1 hover documentation" | |
| const packageDoc2 = "Package lib2 hover documentation" | |
| tests := []struct { | |
| hoverIdent string | |
| want string | |
| wantError bool | |
| }{ | |
| { | |
| "lib1", | |
| packageDoc1, | |
| false, | |
| }, | |
| { | |
| "lib2", | |
| packageDoc2, | |
| false, | |
| }, | |
| { | |
| "lib3", | |
| "", | |
| false, | |
| }, | |
| { | |
| "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(lib1.C) | |
| println(lib2.E) | |
| println(lib3.F) | |
| println(lib4.Z) | |
| } | |
| `, 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.hoverIdent+")\\.")) | |
| if test.wantError { | |
| if err == nil { | |
| t.Errorf("Hover(%q) succeeded unexpectedly", test.hoverIdent) | |
| } | |
| } else if !strings.Contains(got.Value, test.want) { | |
| t.Errorf("Hover(%q): got:\n%q\nwant:\n%q", test.hoverIdent, 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) { | |
| 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", `Hello()\(\) \/\/Here`) // 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 contain 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) { | |
| 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) | |
| } | |
| }) | |
| } | |
| } | |
| const linknameHover = ` | |
| -- go.mod -- | |
| module mod.com | |
| -- upper/upper.go -- | |
| package upper | |
| import ( | |
| _ "unsafe" | |
| _ "mod.com/lower" | |
| ) | |
| //go:linkname foo mod.com/lower.bar | |
| func foo() string | |
| -- lower/lower.go -- | |
| package lower | |
| // bar does foo. | |
| func bar() string { | |
| return "foo by bar" | |
| }` | |
| func TestHoverLinknameDirective(t *testing.T) { | |
| Run(t, linknameHover, func(t *testing.T, env *Env) { | |
| // Jump from directives 2nd arg. | |
| env.OpenFile("upper/upper.go") | |
| from := env.RegexpSearch("upper/upper.go", `lower.bar`) | |
| hover, _ := env.Hover(from) | |
| content := hover.Value | |
| expect := "bar does foo" | |
| if !strings.Contains(content, expect) { | |
| t.Errorf("hover: %q does not contain: %q", content, expect) | |
| } | |
| }) | |
| } | |
| func TestHoverGoWork_Issue60821(t *testing.T) { | |
| const files = ` | |
| -- go.work -- | |
| go 1.19 | |
| use ( | |
| moda | |
| modb | |
| ) | |
| -- moda/go.mod -- | |
| ` | |
| Run(t, files, func(t *testing.T, env *Env) { | |
| env.OpenFile("go.work") | |
| // Neither of the requests below should crash gopls. | |
| _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("go.work", "moda")) | |
| _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("go.work", "modb")) | |
| }) | |
| } | |
| const embedHover = ` | |
| -- go.mod -- | |
| module mod.com | |
| go 1.19 | |
| -- main.go -- | |
| package main | |
| import "embed" | |
| //go:embed *.txt | |
| var foo embed.FS | |
| func main() { | |
| } | |
| -- foo.txt -- | |
| FOO | |
| -- bar.txt -- | |
| BAR | |
| -- baz.txt -- | |
| BAZ | |
| -- other.sql -- | |
| SKIPPED | |
| -- dir.txt/skip.txt -- | |
| SKIPPED | |
| ` | |
| func TestHoverEmbedDirective(t *testing.T) { | |
| Run(t, embedHover, func(t *testing.T, env *Env) { | |
| env.OpenFile("main.go") | |
| from := env.RegexpSearch("main.go", `\*.txt`) | |
| got, _ := env.Hover(from) | |
| if got == nil { | |
| t.Fatalf("hover over //go:embed arg not found") | |
| } | |
| content := got.Value | |
| wants := []string{"foo.txt", "bar.txt", "baz.txt"} | |
| for _, want := range wants { | |
| if !strings.Contains(content, want) { | |
| t.Errorf("hover: %q does not contain: %q", content, want) | |
| } | |
| } | |
| // A directory should never be matched, even if it happens to have a matching name. | |
| // Content in subdirectories should not match on only one asterisk. | |
| skips := []string{"other.sql", "dir.txt", "skip.txt"} | |
| for _, skip := range skips { | |
| if strings.Contains(content, skip) { | |
| t.Errorf("hover: %q should not contain: %q", content, skip) | |
| } | |
| } | |
| }) | |
| } | |
| func TestHoverBrokenImport_Issue60592(t *testing.T) { | |
| const files = ` | |
| -- go.mod -- | |
| module testdata | |
| go 1.18 | |
| -- p.go -- | |
| package main | |
| import foo "a" | |
| func _() { | |
| foo.Print() | |
| } | |
| ` | |
| Run(t, files, func(t *testing.T, env *Env) { | |
| env.OpenFile("p.go") | |
| // This request should not crash gopls. | |
| _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("p.go", "foo[.]")) | |
| }) | |
| } | |
| func TestHoverInternalLinks(t *testing.T) { | |
| const src = ` | |
| -- main.go -- | |
| package main | |
| import "errors" | |
| func main() { | |
| errors.New("oops") | |
| } | |
| ` | |
| for _, test := range []struct { | |
| linksInHover any // JSON configuration value | |
| wantRE string // pattern to match the Hover Markdown output | |
| }{ | |
| { | |
| true, // default: use options.LinkTarget domain | |
| regexp.QuoteMeta("[`errors.New` on pkg.go.dev](https://pkg.go.dev/errors#New)"), | |
| }, | |
| { | |
| "gopls", // use gopls' internal viewer | |
| "\\[`errors.New` in gopls doc viewer\\]\\(http://127.0.0.1:[0-9]+/gopls/[^/]+/pkg/errors\\?view=[0-9]+#New\\)", | |
| }, | |
| } { | |
| WithOptions( | |
| Settings{"linksInHover": test.linksInHover}, | |
| ).Run(t, src, func(t *testing.T, env *Env) { | |
| env.OpenFile("main.go") | |
| got, _ := env.Hover(env.RegexpSearch("main.go", "New")) | |
| if m, err := regexp.MatchString(test.wantRE, got.Value); err != nil { | |
| t.Fatalf("bad regexp in test: %v", err) | |
| } else if !m { | |
| t.Fatalf("hover output does not match %q; got:\n\n%s", test.wantRE, got.Value) | |
| } | |
| }) | |
| } | |
| } | |
| func TestHoverInternalLinksIssue68116(t *testing.T) { | |
| // Links for the internal viewer should not include a module version suffix: | |
| // the package path and the view are an unambiguous key; see #68116. | |
| const proxy = ` | |
| -- example.com@v1.2.3/go.mod -- | |
| module example.com | |
| go 1.12 | |
| -- example.com@v1.2.3/a/a.go -- | |
| package a | |
| // F is a function. | |
| func F() | |
| ` | |
| const mod = ` | |
| -- go.mod -- | |
| module main | |
| go 1.12 | |
| require example.com v1.2.3 | |
| -- main.go -- | |
| package main | |
| import "example.com/a" | |
| func main() { | |
| a.F() | |
| } | |
| ` | |
| WithOptions( | |
| ProxyFiles(proxy), | |
| Settings{"linksInHover": "gopls"}, | |
| WriteGoSum("."), | |
| ).Run(t, mod, func(t *testing.T, env *Env) { | |
| env.OpenFile("main.go") | |
| got, _ := env.Hover(env.RegexpSearch("main.go", "F")) | |
| const wantRE = "\\[`a.F` in gopls doc viewer\\]\\(http://127.0.0.1:[0-9]+/gopls/[^/]+/pkg/example.com/a\\?view=[0-9]+#F\\)" // no version | |
| if m, err := regexp.MatchString(wantRE, got.Value); err != nil { | |
| t.Fatalf("bad regexp in test: %v", err) | |
| } else if !m { | |
| t.Fatalf("hover output does not match %q; got:\n\n%s", wantRE, got.Value) | |
| } | |
| }) | |
| } | |
| func TestHoverBuiltinFile(t *testing.T) { | |
| // This test verifies that hovering in the builtin file provides the same | |
| // hover content as hovering over a use of a builtin. | |
| const src = ` | |
| -- p.go -- | |
| package p | |
| func _() { | |
| const ( | |
| _ = iota | |
| _ = true | |
| ) | |
| var ( | |
| _ any | |
| err error = e{} // avoid nil deref warning | |
| ) | |
| _ = err.Error | |
| println("Hello") | |
| _ = min(1, 2) | |
| } | |
| // e implements Error, for use above. | |
| type e struct{} | |
| func (e) Error() string | |
| ` | |
| // Test hovering over various builtins with different kinds of declarations. | |
| tests := []string{ | |
| "iota", | |
| "true", | |
| "any", | |
| "error", | |
| "Error", | |
| "println", | |
| "min", | |
| } | |
| Run(t, src, func(t *testing.T, env *Env) { | |
| env.OpenFile("p.go") | |
| env.AfterChange(NoDiagnostics()) // avoid accidental compiler errors | |
| for _, builtin := range tests { | |
| useLocation := env.RegexpSearch("p.go", builtin) | |
| calleeHover, _ := env.Hover(useLocation) | |
| declLocation := env.FirstDefinition(useLocation) | |
| env.OpenFile(env.Sandbox.Workdir.URIToPath(declLocation.URI)) | |
| declHover, _ := env.Hover(declLocation) | |
| if diff := cmp.Diff(calleeHover, declHover); diff != "" { | |
| t.Errorf("Hover mismatch (-callee hover +decl hover):\n%s", diff) | |
| } | |
| } | |
| }) | |
| } | |
| func TestHoverStdlibWithAvailableVersion(t *testing.T) { | |
| const src = ` | |
| -- stdlib.go -- | |
| package stdlib | |
| import "fmt" | |
| import "context" | |
| import "crypto" | |
| import "regexp" | |
| import "go/doc/comment" | |
| type testRegexp = *regexp.Regexp | |
| func _() { | |
| var ctx context.Context | |
| ctx = context.Background() | |
| if ctx.Err(); e == context.Canceled { | |
| fmt.Println("Canceled") | |
| fmt.Printf("%v", crypto.SHA512_224) | |
| } | |
| _ := fmt.Appendf(make([]byte, 100), "world, %d", 23) | |
| var re = regexp.MustCompile("\n{2,}") | |
| copy := re.Copy() | |
| var testRE testRegexp | |
| testRE.Longest() | |
| var pr comment.Printer | |
| pr.HeadingID = func(*comment.Heading) string { return "" } | |
| } | |
| ` | |
| testcases := []struct { | |
| symbolRE string // regexp matching symbol to hover over | |
| shouldContain bool | |
| targetString string | |
| }{ | |
| {"Println", false, "go1.0"}, // package-level func | |
| {"Appendf", true, "go1.19"}, // package-level func | |
| {"Background", true, "go1.7"}, // package-level func | |
| {"Canceled", true, "go1.7"}, // package-level var | |
| {"Context", true, "go1.7"}, // package-level type | |
| {"SHA512_224", true, "go1.5"}, // package-level const | |
| {"Copy", true, "go1.6"}, // method | |
| {"Longest", true, "go1.1"}, // method with alias receiver | |
| {"HeadingID", true, "go1.19"}, // field | |
| } | |
| Run(t, src, func(t *testing.T, env *Env) { | |
| env.OpenFile("stdlib.go") | |
| for _, tc := range testcases { | |
| content, _ := env.Hover(env.RegexpSearch("stdlib.go", tc.symbolRE)) | |
| if tc.shouldContain && !strings.Contains(content.Value, tc.targetString) { | |
| t.Errorf("Hover(%q) should contain string %s", tc.symbolRE, tc.targetString) | |
| } | |
| if !tc.shouldContain && strings.Contains(content.Value, tc.targetString) { | |
| t.Errorf("Hover(%q) should not contain string %s", tc.symbolRE, tc.targetString) | |
| } | |
| } | |
| }) | |
| } |