go/internal/lsp/tests/tests.go
Muir Manders 9d59f9e855 internal/lsp: improve completion support for untyped constants
Add some extra smarts when evaluating untyped constants as completion
candidates. Previously we called types.Default() on the expected type
and candidate type, but this loses the untypedness of an untyped
constant which prevents it from being assignable to any type or named
type other than the untyped constant's default type.

Note that the added logic does not take into account the untyped
constant's value, so you will still get some false positive
completions (e.g. suggesting an untyped negative integer constant when
only a uint would do). Unfortunately go/types doesn't provide a way of
answering the question "is this *types.Const assignable to this
types.Type" since types.AssignableTo only considers a constant's type,
not its value.

Change-Id: If7075642e928f712b127256ae7706a5190e2f42c
GitHub-Last-Rev: 124d2f05b0aec09c9d7004d9da0d900524185b92
GitHub-Pull-Request: golang/tools#128
Reviewed-on: https://go-review.googlesource.com/c/tools/+/184477
Reviewed-by: Suzy Mueller <suzmue@golang.org>
2019-07-03 16:30:32 +00:00

533 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 = 144
ExpectedCompletionSnippetCount = 15
ExpectedDiagnosticsCount = 17
ExpectedFormatCount = 5
ExpectedImportCount = 2
ExpectedDefinitionsCount = 38
ExpectedTypeDefinitionsCount = 2
ExpectedHighlightsCount = 2
ExpectedReferencesCount = 4
ExpectedRenamesCount = 11
ExpectedSymbolsCount = 1
ExpectedSignaturesCount = 21
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),
}
// Hardcode special case to test the lack of a signature.
if signature == "" && activeParam == 0 {
data.Signatures[spn] = nil
}
}
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,
})
}