gopls/internal/regtest: add regression tests for template diagnostics

Add some additional regressions tests for our loading of templates based
on language ID and/or the configured templateExtensions. Move these
tests to a new regtest package "templates", so that they may be easily
run together.

Fixes golang/vscode-go#1957

Change-Id: Ic83454725e9aec41b3c1f5202bb68d97cc73c839
Reviewed-on: https://go-review.googlesource.com/c/tools/+/378394
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
This commit is contained in:
Robert Findley 2022-01-13 13:45:37 -05:00
parent c4cfc425f2
commit a222cdb107
3 changed files with 189 additions and 91 deletions

View File

@ -1,75 +0,0 @@
// 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 (
"strings"
"testing"
"golang.org/x/tools/internal/lsp/protocol"
. "golang.org/x/tools/internal/lsp/regtest"
)
const filesA = `
-- go.mod --
module mod.com
go 1.12
-- b.gotmpl --
{{define "A"}}goo{{end}}
-- a.tmpl --
{{template "A"}}
`
func TestSuffixes(t *testing.T) {
WithOptions(
EditorConfig{
AllExperiments: true,
Settings: map[string]interface{}{
"templateExtensions": []string{"tmpl", "gotmpl"},
},
},
).Run(t, filesA, func(t *testing.T, env *Env) {
env.OpenFile("a.tmpl")
x := env.RegexpSearch("a.tmpl", `A`)
file, pos := env.GoToDefinition("a.tmpl", x)
refs := env.References(file, pos)
if len(refs) != 2 {
t.Fatalf("got %v reference(s), want 2", len(refs))
}
// make sure we got one from b.gotmpl
want := env.Sandbox.Workdir.URI("b.gotmpl")
if refs[0].URI != want && refs[1].URI != want {
t.Errorf("failed to find reference to %s", shorten(want))
for i, r := range refs {
t.Logf("%d: URI:%s %v", i, shorten(r.URI), r.Range)
}
}
content, npos := env.Hover(file, pos)
if pos != npos {
t.Errorf("pos? got %v, wanted %v", npos, pos)
}
if content.Value != "template A defined" {
t.Errorf("got %s, wanted 'template A defined", content.Value)
}
})
}
// shorten long URIs
func shorten(fn protocol.DocumentURI) string {
if len(fn) <= 20 {
return string(fn)
}
pieces := strings.Split(string(fn), "/")
if len(pieces) < 2 {
return string(fn)
}
j := len(pieces)
return pieces[j-2] + "/" + pieces[j-1]
}
// Hover, SemTok, Diagnose with errors
// and better coverage

View File

@ -0,0 +1,150 @@
// 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 template
import (
"strings"
"testing"
"golang.org/x/tools/gopls/internal/hooks"
"golang.org/x/tools/internal/lsp/protocol"
. "golang.org/x/tools/internal/lsp/regtest"
)
func TestMain(m *testing.M) {
Main(m, hooks.Options)
}
func TestTemplatesFromExtensions(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- hello.tmpl --
{{range .Planets}}
Hello {{}} <-- missing body
{{end}}
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"templateExtensions": []string{"tmpl"},
},
},
).Run(t, files, func(t *testing.T, env *Env) {
// TODO: can we move this diagnostic onto {{}}?
env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"))
env.WriteWorkspaceFile("hello.tmpl", "{{range .Planets}}\nHello {{.}}\n{{end}}")
env.Await(EmptyDiagnostics("hello.tmpl"))
})
}
func TestTemplatesFromLangID(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
`
Run(t, files, func(t *testing.T, env *Env) {
env.CreateBuffer("hello.tmpl", "")
env.Await(
OnceMet(
env.DoneWithOpen(),
NoDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates.
),
)
env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}")
env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"))
env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}")
env.Await(EmptyOrNoDiagnostics("hello.tmpl"))
})
}
func TestClosingTemplatesMakesDiagnosticsDisappear(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- hello.tmpl --
{{range .Planets}}
Hello {{}} <-- missing body
{{end}}
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("hello.tmpl")
env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"))
// Since we don't have templateExtensions configured, closing hello.tmpl
// should make its diagnostics disappear.
env.CloseBuffer("hello.tmpl")
env.Await(EmptyDiagnostics("hello.tmpl"))
})
}
func TestMultipleSuffixes(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- b.gotmpl --
{{define "A"}}goo{{end}}
-- a.tmpl --
{{template "A"}}
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"templateExtensions": []string{"tmpl", "gotmpl"},
},
},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a.tmpl")
x := env.RegexpSearch("a.tmpl", `A`)
file, pos := env.GoToDefinition("a.tmpl", x)
refs := env.References(file, pos)
if len(refs) != 2 {
t.Fatalf("got %v reference(s), want 2", len(refs))
}
// make sure we got one from b.gotmpl
want := env.Sandbox.Workdir.URI("b.gotmpl")
if refs[0].URI != want && refs[1].URI != want {
t.Errorf("failed to find reference to %s", shorten(want))
for i, r := range refs {
t.Logf("%d: URI:%s %v", i, shorten(r.URI), r.Range)
}
}
content, npos := env.Hover(file, pos)
if pos != npos {
t.Errorf("pos? got %v, wanted %v", npos, pos)
}
if content.Value != "template A defined" {
t.Errorf("got %s, wanted 'template A defined", content.Value)
}
})
}
// shorten long URIs
func shorten(fn protocol.DocumentURI) string {
if len(fn) <= 20 {
return string(fn)
}
pieces := strings.Split(string(fn), "/")
if len(pieces) < 2 {
return string(fn)
}
j := len(pieces)
return pieces[j-2] + "/" + pieces[j-1]
}
// Hover, SemTok, Diagnose with errors
// and better coverage

View File

@ -9,6 +9,7 @@ import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
@ -114,6 +115,14 @@ type EditorConfig struct {
// Whether to edit files with windows line endings.
WindowsLineEndings bool
// Map of language ID -> regexp to match, used to set the file type of new
// buffers. Applied as an overlay on top of the following defaults:
// "go" -> ".*\.go"
// "go.mod" -> "go\.mod"
// "go.sum" -> "go\.sum"
// "gotmpl" -> ".*tmpl"
FileAssociations map[string]string
// Settings holds arbitrary additional settings to apply to the gopls config.
// TODO(rfindley): replace existing EditorConfig fields with Settings.
Settings map[string]interface{}
@ -378,21 +387,6 @@ func (e *Editor) OpenFile(ctx context.Context, path string) error {
return e.createBuffer(ctx, path, false, content)
}
func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
uri := wd.URI(buf.path)
languageID := ""
if strings.HasSuffix(buf.path, ".go") {
// TODO: what about go.mod files? What is their language ID?
languageID = "go"
}
return protocol.TextDocumentItem{
URI: uri,
LanguageID: languageID,
Version: int32(buf.version),
Text: buf.text(),
}
}
// CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
// containing the given textual content.
func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
@ -410,7 +404,13 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont
e.mu.Lock()
defer e.mu.Unlock()
e.buffers[path] = buf
item := textDocumentItem(e.sandbox.Workdir, buf)
item := protocol.TextDocumentItem{
URI: e.sandbox.Workdir.URI(buf.path),
LanguageID: e.languageID(buf.path),
Version: int32(buf.version),
Text: buf.text(),
}
if e.Server != nil {
if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
@ -425,6 +425,29 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont
return nil
}
var defaultFileAssociations = map[string]*regexp.Regexp{
"go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl!
"go.mod": regexp.MustCompile(`^go\.mod$`),
"go.sum": regexp.MustCompile(`^go\.sum$`),
"gotmpl": regexp.MustCompile(`^.*tmpl$`),
}
func (e *Editor) languageID(p string) string {
base := path.Base(p)
for lang, re := range e.Config.FileAssociations {
re := regexp.MustCompile(re)
if re.MatchString(base) {
return lang
}
}
for lang, re := range defaultFileAssociations {
if re.MatchString(base) {
return lang
}
}
return ""
}
// lines returns line-ending agnostic line representation of content.
func lines(content string) []string {
lines := strings.Split(content, "\n")