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>
diff --git a/gopls/internal/regtest/misc/template_test.go b/gopls/internal/regtest/misc/template_test.go
deleted file mode 100644
index 6d1419a..0000000
--- a/gopls/internal/regtest/misc/template_test.go
+++ /dev/null
@@ -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
diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go
new file mode 100644
index 0000000..6f4ec01
--- /dev/null
+++ b/gopls/internal/regtest/template/template_test.go
@@ -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
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index fe7d4b5..e47a1bf 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -9,6 +9,7 @@
"context"
"fmt"
"os"
+ "path"
"path/filepath"
"regexp"
"strings"
@@ -114,6 +115,14 @@
// 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 @@
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 @@
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 @@
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")