mirror of
https://github.com/golang/go
synced 2024-11-19 00:44:40 -07:00
431033348d
Previously we would always expand *types.Func completion candidates to function calls, even if the expected type matched the function itself, not its return value. Now we check the function itself before we check its return value. This fixes cases like this: func foo() int { return 0 } var f func() int f = <foo> // now completes to "foo" instead of "foo()" Also, *types.Var function values were never getting expanded to calls. I fixed the completion formatting to know that both *types.Func and *types.Var objects might need to be invoked in the completion item. This fixes cases like this: foo := func() int { return 0 } var i int i = <foo()> // now completes to "foo()" instead of "foo" Change-Id: I8d0e9e2774f92866a3dd881092c13019fb3f3fd5 GitHub-Last-Rev: 7442bc84b5bbb86296289bbc745ec56a5f89d901 GitHub-Pull-Request: golang/tools#122 Reviewed-on: https://go-review.googlesource.com/c/tools/+/182879 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
529 lines
15 KiB
Go
529 lines
15 KiB
Go
// Copyright 2019 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package tests
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"go/ast"
|
|
"go/token"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/tools/go/packages"
|
|
"golang.org/x/tools/go/packages/packagestest"
|
|
"golang.org/x/tools/internal/lsp/source"
|
|
"golang.org/x/tools/internal/span"
|
|
"golang.org/x/tools/internal/txtar"
|
|
)
|
|
|
|
// We hardcode the expected number of test cases to ensure that all tests
|
|
// are being executed. If a test is added, this number must be changed.
|
|
const (
|
|
ExpectedCompletionsCount = 127
|
|
ExpectedCompletionSnippetCount = 14
|
|
ExpectedDiagnosticsCount = 17
|
|
ExpectedFormatCount = 5
|
|
ExpectedImportCount = 2
|
|
ExpectedDefinitionsCount = 38
|
|
ExpectedTypeDefinitionsCount = 2
|
|
ExpectedHighlightsCount = 2
|
|
ExpectedReferencesCount = 2
|
|
ExpectedRenamesCount = 4
|
|
ExpectedSymbolsCount = 1
|
|
ExpectedSignaturesCount = 20
|
|
ExpectedLinksCount = 2
|
|
)
|
|
|
|
const (
|
|
overlayFileSuffix = ".overlay"
|
|
goldenFileSuffix = ".golden"
|
|
inFileSuffix = ".in"
|
|
testModule = "golang.org/x/tools/internal/lsp"
|
|
)
|
|
|
|
var updateGolden = flag.Bool("golden", false, "Update golden files")
|
|
|
|
type Diagnostics map[span.URI][]source.Diagnostic
|
|
type CompletionItems map[token.Pos]*source.CompletionItem
|
|
type Completions map[span.Span][]token.Pos
|
|
type CompletionSnippets map[span.Span]CompletionSnippet
|
|
type Formats []span.Span
|
|
type Imports []span.Span
|
|
type Definitions map[span.Span]Definition
|
|
type Highlights map[string][]span.Span
|
|
type References map[span.Span][]span.Span
|
|
type Renames map[span.Span]string
|
|
type Symbols map[span.URI][]source.Symbol
|
|
type SymbolsChildren map[string][]source.Symbol
|
|
type Signatures map[span.Span]source.SignatureInformation
|
|
type Links map[span.URI][]Link
|
|
|
|
type Data struct {
|
|
Config packages.Config
|
|
Exported *packagestest.Exported
|
|
Diagnostics Diagnostics
|
|
CompletionItems CompletionItems
|
|
Completions Completions
|
|
CompletionSnippets CompletionSnippets
|
|
Formats Formats
|
|
Imports Imports
|
|
Definitions Definitions
|
|
Highlights Highlights
|
|
References References
|
|
Renames Renames
|
|
Symbols Symbols
|
|
symbolsChildren SymbolsChildren
|
|
Signatures Signatures
|
|
Links Links
|
|
|
|
t testing.TB
|
|
fragments map[string]string
|
|
dir string
|
|
golden map[string]*Golden
|
|
}
|
|
|
|
type Tests interface {
|
|
Diagnostics(*testing.T, Diagnostics)
|
|
Completion(*testing.T, Completions, CompletionSnippets, CompletionItems)
|
|
Format(*testing.T, Formats)
|
|
Import(*testing.T, Imports)
|
|
Definition(*testing.T, Definitions)
|
|
Highlight(*testing.T, Highlights)
|
|
Reference(*testing.T, References)
|
|
Rename(*testing.T, Renames)
|
|
Symbol(*testing.T, Symbols)
|
|
SignatureHelp(*testing.T, Signatures)
|
|
Link(*testing.T, Links)
|
|
}
|
|
|
|
type Definition struct {
|
|
Name string
|
|
Src span.Span
|
|
IsType bool
|
|
OnlyHover bool
|
|
Def span.Span
|
|
}
|
|
|
|
type CompletionSnippet struct {
|
|
CompletionItem token.Pos
|
|
PlainSnippet string
|
|
PlaceholderSnippet string
|
|
}
|
|
|
|
type Link struct {
|
|
Src span.Span
|
|
Target string
|
|
}
|
|
|
|
type Golden struct {
|
|
Filename string
|
|
Archive *txtar.Archive
|
|
Modified bool
|
|
}
|
|
|
|
func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
|
|
t.Helper()
|
|
|
|
data := &Data{
|
|
Diagnostics: make(Diagnostics),
|
|
CompletionItems: make(CompletionItems),
|
|
Completions: make(Completions),
|
|
CompletionSnippets: make(CompletionSnippets),
|
|
Definitions: make(Definitions),
|
|
Highlights: make(Highlights),
|
|
References: make(References),
|
|
Renames: make(Renames),
|
|
Symbols: make(Symbols),
|
|
symbolsChildren: make(SymbolsChildren),
|
|
Signatures: make(Signatures),
|
|
Links: make(Links),
|
|
|
|
t: t,
|
|
dir: dir,
|
|
fragments: map[string]string{},
|
|
golden: map[string]*Golden{},
|
|
}
|
|
|
|
files := packagestest.MustCopyFileTree(dir)
|
|
overlays := map[string][]byte{}
|
|
for fragment, operation := range files {
|
|
if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment {
|
|
delete(files, fragment)
|
|
goldFile := filepath.Join(dir, fragment)
|
|
archive, err := txtar.ParseFile(goldFile)
|
|
if err != nil {
|
|
t.Fatalf("could not read golden file %v: %v", fragment, err)
|
|
}
|
|
data.golden[trimmed] = &Golden{
|
|
Filename: goldFile,
|
|
Archive: archive,
|
|
}
|
|
} else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment {
|
|
delete(files, fragment)
|
|
files[trimmed] = operation
|
|
} else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 {
|
|
delete(files, fragment)
|
|
partial := fragment[:index] + fragment[index+len(overlayFileSuffix):]
|
|
contents, err := ioutil.ReadFile(filepath.Join(dir, fragment))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
overlays[partial] = contents
|
|
}
|
|
}
|
|
modules := []packagestest.Module{
|
|
{
|
|
Name: testModule,
|
|
Files: files,
|
|
Overlay: overlays,
|
|
},
|
|
}
|
|
data.Exported = packagestest.Export(t, exporter, modules)
|
|
for fragment, _ := range files {
|
|
filename := data.Exported.File(testModule, fragment)
|
|
data.fragments[filename] = fragment
|
|
}
|
|
|
|
// Merge the exported.Config with the view.Config.
|
|
data.Config = *data.Exported.Config
|
|
data.Config.Fset = token.NewFileSet()
|
|
data.Config.Context = context.Background()
|
|
data.Config.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
|
|
panic("ParseFile should not be called")
|
|
}
|
|
|
|
// Do a first pass to collect special markers for completion.
|
|
if err := data.Exported.Expect(map[string]interface{}{
|
|
"item": func(name string, r packagestest.Range, _, _ string) {
|
|
data.Exported.Mark(name, r)
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Collect any data that needs to be used by subsequent tests.
|
|
if err := data.Exported.Expect(map[string]interface{}{
|
|
"diag": data.collectDiagnostics,
|
|
"item": data.collectCompletionItems,
|
|
"complete": data.collectCompletions,
|
|
"format": data.collectFormats,
|
|
"import": data.collectImports,
|
|
"godef": data.collectDefinitions,
|
|
"typdef": data.collectTypeDefinitions,
|
|
"hover": data.collectHoverDefinitions,
|
|
"highlight": data.collectHighlights,
|
|
"refs": data.collectReferences,
|
|
"rename": data.collectRenames,
|
|
"symbol": data.collectSymbols,
|
|
"signature": data.collectSignatures,
|
|
"snippet": data.collectCompletionSnippets,
|
|
"link": data.collectLinks,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, symbols := range data.Symbols {
|
|
for i := range symbols {
|
|
children := data.symbolsChildren[symbols[i].Name]
|
|
symbols[i].Children = children
|
|
}
|
|
}
|
|
// Collect names for the entries that require golden files.
|
|
if err := data.Exported.Expect(map[string]interface{}{
|
|
"godef": data.collectDefinitionNames,
|
|
"hover": data.collectDefinitionNames,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func Run(t *testing.T, tests Tests, data *Data) {
|
|
t.Helper()
|
|
t.Run("Completion", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Completions) != ExpectedCompletionsCount {
|
|
t.Errorf("got %v completions expected %v", len(data.Completions), ExpectedCompletionsCount)
|
|
}
|
|
if len(data.CompletionSnippets) != ExpectedCompletionSnippetCount {
|
|
t.Errorf("got %v snippets expected %v", len(data.CompletionSnippets), ExpectedCompletionSnippetCount)
|
|
}
|
|
tests.Completion(t, data.Completions, data.CompletionSnippets, data.CompletionItems)
|
|
})
|
|
|
|
t.Run("Diagnostics", func(t *testing.T) {
|
|
t.Helper()
|
|
diagnosticsCount := 0
|
|
for _, want := range data.Diagnostics {
|
|
diagnosticsCount += len(want)
|
|
}
|
|
if diagnosticsCount != ExpectedDiagnosticsCount {
|
|
t.Errorf("got %v diagnostics expected %v", diagnosticsCount, ExpectedDiagnosticsCount)
|
|
}
|
|
tests.Diagnostics(t, data.Diagnostics)
|
|
})
|
|
|
|
t.Run("Format", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Formats) != ExpectedFormatCount {
|
|
t.Errorf("got %v formats expected %v", len(data.Formats), ExpectedFormatCount)
|
|
}
|
|
tests.Format(t, data.Formats)
|
|
})
|
|
|
|
t.Run("Import", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Imports) != ExpectedImportCount {
|
|
t.Errorf("got %v imports expected %v", len(data.Imports), ExpectedImportCount)
|
|
}
|
|
tests.Import(t, data.Imports)
|
|
})
|
|
|
|
t.Run("Definition", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Definitions) != ExpectedDefinitionsCount {
|
|
t.Errorf("got %v definitions expected %v", len(data.Definitions), ExpectedDefinitionsCount)
|
|
}
|
|
tests.Definition(t, data.Definitions)
|
|
})
|
|
|
|
t.Run("Highlight", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Highlights) != ExpectedHighlightsCount {
|
|
t.Errorf("got %v highlights expected %v", len(data.Highlights), ExpectedHighlightsCount)
|
|
}
|
|
tests.Highlight(t, data.Highlights)
|
|
})
|
|
|
|
t.Run("References", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.References) != ExpectedReferencesCount {
|
|
t.Errorf("got %v references expected %v", len(data.References), ExpectedReferencesCount)
|
|
}
|
|
tests.Reference(t, data.References)
|
|
})
|
|
|
|
t.Run("Renames", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Renames) != ExpectedRenamesCount {
|
|
t.Errorf("got %v renames expected %v", len(data.Renames), ExpectedRenamesCount)
|
|
}
|
|
tests.Rename(t, data.Renames)
|
|
})
|
|
|
|
t.Run("Symbols", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Symbols) != ExpectedSymbolsCount {
|
|
t.Errorf("got %v symbols expected %v", len(data.Symbols), ExpectedSymbolsCount)
|
|
}
|
|
tests.Symbol(t, data.Symbols)
|
|
})
|
|
|
|
t.Run("SignatureHelp", func(t *testing.T) {
|
|
t.Helper()
|
|
if len(data.Signatures) != ExpectedSignaturesCount {
|
|
t.Errorf("got %v signatures expected %v", len(data.Signatures), ExpectedSignaturesCount)
|
|
}
|
|
tests.SignatureHelp(t, data.Signatures)
|
|
})
|
|
|
|
t.Run("Link", func(t *testing.T) {
|
|
t.Helper()
|
|
linksCount := 0
|
|
for _, want := range data.Links {
|
|
linksCount += len(want)
|
|
}
|
|
if linksCount != ExpectedLinksCount {
|
|
t.Errorf("got %v links expected %v", linksCount, ExpectedLinksCount)
|
|
}
|
|
tests.Link(t, data.Links)
|
|
})
|
|
|
|
if *updateGolden {
|
|
for _, golden := range data.golden {
|
|
if !golden.Modified {
|
|
continue
|
|
}
|
|
sort.Slice(golden.Archive.Files, func(i, j int) bool {
|
|
return golden.Archive.Files[i].Name < golden.Archive.Files[j].Name
|
|
})
|
|
if err := ioutil.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (data *Data) Golden(tag string, target string, update func() ([]byte, error)) []byte {
|
|
data.t.Helper()
|
|
fragment, found := data.fragments[target]
|
|
if !found {
|
|
if filepath.IsAbs(target) {
|
|
data.t.Fatalf("invalid golden file fragment %v", target)
|
|
}
|
|
fragment = target
|
|
}
|
|
golden := data.golden[fragment]
|
|
if golden == nil {
|
|
if !*updateGolden {
|
|
data.t.Fatalf("could not find golden file %v: %v", fragment, tag)
|
|
}
|
|
golden = &Golden{
|
|
Filename: filepath.Join(data.dir, fragment+goldenFileSuffix),
|
|
Archive: &txtar.Archive{},
|
|
Modified: true,
|
|
}
|
|
data.golden[fragment] = golden
|
|
}
|
|
var file *txtar.File
|
|
for i := range golden.Archive.Files {
|
|
f := &golden.Archive.Files[i]
|
|
if f.Name == tag {
|
|
file = f
|
|
break
|
|
}
|
|
}
|
|
if *updateGolden {
|
|
if file == nil {
|
|
golden.Archive.Files = append(golden.Archive.Files, txtar.File{
|
|
Name: tag,
|
|
})
|
|
file = &golden.Archive.Files[len(golden.Archive.Files)-1]
|
|
}
|
|
contents, err := update()
|
|
if err != nil {
|
|
data.t.Fatalf("could not update golden file %v: %v", fragment, err)
|
|
}
|
|
file.Data = append(contents, '\n') // add trailing \n for txtar
|
|
golden.Modified = true
|
|
}
|
|
if file == nil {
|
|
data.t.Fatalf("could not find golden contents %v: %v", fragment, tag)
|
|
}
|
|
return file.Data[:len(file.Data)-1] // drop the trailing \n
|
|
}
|
|
|
|
func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) {
|
|
if _, ok := data.Diagnostics[spn.URI()]; !ok {
|
|
data.Diagnostics[spn.URI()] = []source.Diagnostic{}
|
|
}
|
|
// If a file has an empty diagnostic message, return. This allows us to
|
|
// avoid testing diagnostics in files that may have a lot of them.
|
|
if msg == "" {
|
|
return
|
|
}
|
|
severity := source.SeverityError
|
|
if strings.Contains(string(spn.URI()), "analyzer") {
|
|
severity = source.SeverityWarning
|
|
}
|
|
want := source.Diagnostic{
|
|
Span: spn,
|
|
Severity: severity,
|
|
Source: msgSource,
|
|
Message: msg,
|
|
}
|
|
data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want)
|
|
}
|
|
|
|
func (data *Data) collectCompletions(src span.Span, expected []token.Pos) {
|
|
data.Completions[src] = expected
|
|
}
|
|
|
|
func (data *Data) collectCompletionItems(pos token.Pos, label, detail, kind string) {
|
|
data.CompletionItems[pos] = &source.CompletionItem{
|
|
Label: label,
|
|
Detail: detail,
|
|
Kind: source.ParseCompletionItemKind(kind),
|
|
}
|
|
}
|
|
|
|
func (data *Data) collectFormats(spn span.Span) {
|
|
data.Formats = append(data.Formats, spn)
|
|
}
|
|
|
|
func (data *Data) collectImports(spn span.Span) {
|
|
data.Imports = append(data.Imports, spn)
|
|
}
|
|
|
|
func (data *Data) collectDefinitions(src, target span.Span) {
|
|
data.Definitions[src] = Definition{
|
|
Src: src,
|
|
Def: target,
|
|
}
|
|
}
|
|
|
|
func (data *Data) collectHoverDefinitions(src, target span.Span) {
|
|
data.Definitions[src] = Definition{
|
|
Src: src,
|
|
Def: target,
|
|
OnlyHover: true,
|
|
}
|
|
}
|
|
|
|
func (data *Data) collectTypeDefinitions(src, target span.Span) {
|
|
data.Definitions[src] = Definition{
|
|
Src: src,
|
|
Def: target,
|
|
IsType: true,
|
|
}
|
|
}
|
|
|
|
func (data *Data) collectDefinitionNames(src span.Span, name string) {
|
|
d := data.Definitions[src]
|
|
d.Name = name
|
|
data.Definitions[src] = d
|
|
}
|
|
|
|
func (data *Data) collectHighlights(name string, rng span.Span) {
|
|
data.Highlights[name] = append(data.Highlights[name], rng)
|
|
}
|
|
|
|
func (data *Data) collectReferences(src span.Span, expected []span.Span) {
|
|
data.References[src] = expected
|
|
}
|
|
|
|
func (data *Data) collectRenames(src span.Span, newText string) {
|
|
data.Renames[src] = newText
|
|
}
|
|
|
|
func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string) {
|
|
sym := source.Symbol{
|
|
Name: name,
|
|
Kind: source.ParseSymbolKind(kind),
|
|
SelectionSpan: spn,
|
|
}
|
|
if parentName == "" {
|
|
data.Symbols[spn.URI()] = append(data.Symbols[spn.URI()], sym)
|
|
} else {
|
|
data.symbolsChildren[parentName] = append(data.symbolsChildren[parentName], sym)
|
|
}
|
|
}
|
|
|
|
func (data *Data) collectSignatures(spn span.Span, signature string, activeParam int64) {
|
|
data.Signatures[spn] = source.SignatureInformation{
|
|
Label: signature,
|
|
ActiveParameter: int(activeParam),
|
|
}
|
|
}
|
|
|
|
func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain, placeholder string) {
|
|
data.CompletionSnippets[spn] = CompletionSnippet{
|
|
CompletionItem: item,
|
|
PlainSnippet: plain,
|
|
PlaceholderSnippet: placeholder,
|
|
}
|
|
}
|
|
|
|
func (data *Data) collectLinks(spn span.Span, link string) {
|
|
uri := spn.URI()
|
|
data.Links[uri] = append(data.Links[uri], Link{
|
|
Src: spn,
|
|
Target: link,
|
|
})
|
|
}
|