internal/lsp: use packagestest markers to test diagnostics

Add some basic tests for diagnostics using the new
go/packages/packagestest framework.

Change-Id: I6a7bfba6c392928a9eb123ab71ceb73785c12600
Reviewed-on: https://go-review.googlesource.com/c/145697
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go
index 2086b1c..c59f7c0 100644
--- a/go/packages/packagestest/export.go
+++ b/go/packages/packagestest/export.go
@@ -217,7 +217,10 @@
 // This will panic if there is any kind of error trying to walk the file tree.
 func MustCopyFileTree(root string) map[string]interface{} {
 	result := map[string]interface{}{}
-	if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+	if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
 		if info.IsDir() {
 			return nil
 		}
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 1a38d73..2ca622b 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -63,23 +63,27 @@
 }
 
 func parseErrorPos(pkgErr packages.Error) (pos token.Position) {
-	split := strings.Split(pkgErr.Pos, ":")
-	if len(split) <= 1 {
-		return pos
-	}
-	pos.Filename = split[0]
-	line, err := strconv.ParseInt(split[1], 10, 64)
-	if err != nil {
-		return pos
-	}
-	pos.Line = int(line)
-	if len(split) == 3 {
-		col, err := strconv.ParseInt(split[2], 10, 64)
-		if err != nil {
-			return pos
-		}
-		pos.Column = int(col)
+	remainder1, first, hasLine := chop(pkgErr.Pos)
+	remainder2, second, hasColumn := chop(remainder1)
+	if hasLine && hasColumn {
+		pos.Filename = remainder2
+		pos.Line = second
+		pos.Column = first
+	} else if hasLine {
+		pos.Filename = remainder1
+		pos.Line = first
 	}
 	return pos
+}
 
+func chop(text string) (remainder string, value int, ok bool) {
+	i := strings.LastIndex(text, ":")
+	if i < 0 {
+		return text, 0, false
+	}
+	v, err := strconv.ParseInt(text[i+1:], 10, 64)
+	if err != nil {
+		return text, 0, false
+	}
+	return text[:i], int(v), true
 }
diff --git a/internal/lsp/diagnostics_test.go b/internal/lsp/diagnostics_test.go
new file mode 100644
index 0000000..9f58584
--- /dev/null
+++ b/internal/lsp/diagnostics_test.go
@@ -0,0 +1,87 @@
+package lsp
+
+import (
+	"go/token"
+	"reflect"
+	"sort"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/go/packages/packagestest"
+	"golang.org/x/tools/internal/lsp/protocol"
+)
+
+func TestDiagnostics(t *testing.T) {
+	packagestest.TestAll(t, testDiagnostics)
+}
+
+func testDiagnostics(t *testing.T, exporter packagestest.Exporter) {
+	files := packagestest.MustCopyFileTree("testdata/diagnostics")
+	// TODO(rstambler): Stop hardcoding this if we have more files that don't parse.
+	files["noparse/noparse.go"] = packagestest.Copy("testdata/diagnostics/noparse/noparse.go.in")
+	modules := []packagestest.Module{
+		{
+			Name:  "golang.org/x/tools/internal/lsp",
+			Files: files,
+		},
+	}
+	exported := packagestest.Export(t, exporter, modules)
+	defer exported.Cleanup()
+
+	wants := make(map[string][]protocol.Diagnostic)
+	for _, module := range modules {
+		for fragment := range module.Files {
+			if !strings.HasSuffix(fragment, ".go") {
+				continue
+			}
+			filename := exporter.Filename(exported, module.Name, fragment)
+			wants[filename] = []protocol.Diagnostic{}
+		}
+	}
+	err := exported.Expect(map[string]interface{}{
+		"diag": func(pos token.Position, msg string) {
+			line := float64(pos.Line - 1)
+			col := float64(pos.Column - 1)
+			want := protocol.Diagnostic{
+				Range: protocol.Range{
+					Start: protocol.Position{
+						Line:      line,
+						Character: col,
+					},
+					End: protocol.Position{
+						Line:      line,
+						Character: col,
+					},
+				},
+				Severity: protocol.SeverityError,
+				Source:   "LSP: Go compiler",
+				Message:  msg,
+			}
+			if _, ok := wants[pos.Filename]; ok {
+				wants[pos.Filename] = append(wants[pos.Filename], want)
+			} else {
+				t.Errorf("unexpected filename: %v", pos.Filename)
+			}
+		},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	v := newView()
+	v.config = exported.Config
+	v.config.Mode = packages.LoadSyntax
+	for filename, want := range wants {
+		diagnostics, err := v.diagnostics(filenameToURI(filename))
+		if err != nil {
+			t.Fatal(err)
+		}
+		got := diagnostics[filename]
+		sort.Slice(got, func(i int, j int) bool {
+			return got[i].Range.Start.Line < got[j].Range.Start.Line
+		})
+		if equal := reflect.DeepEqual(want, got); !equal {
+			t.Errorf("diagnostics failed for %s: (expected: %v), (got: %v)", filename, want, got)
+		}
+	}
+}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index c246270..58cecde 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -16,9 +16,7 @@
 // RunServer starts an LSP server on the supplied stream, and waits until the
 // stream is closed.
 func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) error {
-	s := &server{
-		view: newView(),
-	}
+	s := &server{}
 	conn, client := protocol.RunServer(ctx, stream, s, opts...)
 	s.client = client
 	return conn.Wait(ctx)
@@ -39,6 +37,7 @@
 	if s.initialized {
 		return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
 	}
+	s.view = newView()
 	s.initialized = true
 	return &protocol.InitializeResult{
 		Capabilities: protocol.ServerCapabilities{
diff --git a/internal/lsp/testdata/diagnostics/bad/bad.go b/internal/lsp/testdata/diagnostics/bad/bad.go
new file mode 100644
index 0000000..abbf57e
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/bad/bad.go
@@ -0,0 +1,19 @@
+package bad
+
+func stuff() {
+	x := "heeeeyyyy"
+	random2(x) //@diag("x", "cannot use x (variable of type string) as int value in argument to random2")
+	random2(1)
+	y := 3 //@diag("y", "y declared but not used")
+}
+
+type bob struct {
+	x int
+}
+
+func _() {
+	var q int
+	_ = &bob{
+		f: q, //@diag("f", "unknown field f in struct literal")
+	}
+}
diff --git a/internal/lsp/testdata/diagnostics/bad/bad_util.go b/internal/lsp/testdata/diagnostics/bad/bad_util.go
new file mode 100644
index 0000000..9ab8a73
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/bad/bad_util.go
@@ -0,0 +1,6 @@
+package bad
+
+func random2(y int) int {
+	x := 6 //@diag("x", "x declared but not used")
+	return y
+}
diff --git a/internal/lsp/testdata/diagnostics/bar/bar.go b/internal/lsp/testdata/diagnostics/bar/bar.go
new file mode 100644
index 0000000..f9f99af
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/bar/bar.go
@@ -0,0 +1,7 @@
+package bar
+
+import "golang.org/x/tools/internal/lsp/foo"
+
+func Bar() {
+	foo.Foo()
+}
diff --git a/internal/lsp/testdata/diagnostics/baz/baz.go b/internal/lsp/testdata/diagnostics/baz/baz.go
new file mode 100644
index 0000000..3b95ee2
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/baz/baz.go
@@ -0,0 +1,7 @@
+package baz
+
+import "golang.org/x/tools/internal/lsp/bar"
+
+func Baz() {
+	bar.Bar()
+}
diff --git a/internal/lsp/testdata/diagnostics/foo/foo.go b/internal/lsp/testdata/diagnostics/foo/foo.go
new file mode 100644
index 0000000..18be5d3
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/foo/foo.go
@@ -0,0 +1,3 @@
+package foo
+
+func Foo() {}
diff --git a/internal/lsp/testdata/diagnostics/good/good.go b/internal/lsp/testdata/diagnostics/good/good.go
new file mode 100644
index 0000000..14b41e6
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/good/good.go
@@ -0,0 +1,6 @@
+package good
+
+func stuff() {
+	x := 5
+	random2(x)
+}
diff --git a/internal/lsp/testdata/diagnostics/good/good_util.go b/internal/lsp/testdata/diagnostics/good/good_util.go
new file mode 100644
index 0000000..ae71ad4
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/good/good_util.go
@@ -0,0 +1,10 @@
+package good
+
+func random() int {
+	y := 6 + 7
+	return y
+}
+
+func random2(y int) int {
+	return y
+}
diff --git a/internal/lsp/testdata/diagnostics/noparse/noparse.go.in b/internal/lsp/testdata/diagnostics/noparse/noparse.go.in
new file mode 100644
index 0000000..9dede9c
--- /dev/null
+++ b/internal/lsp/testdata/diagnostics/noparse/noparse.go.in
@@ -0,0 +1,11 @@
+package noparse
+
+func bye(x int) {
+	hi()
+}
+
+func stuff() {
+	x := 5
+}
+
+func .() {} //@diag(".", "expected 'IDENT', found '.'")
diff --git a/internal/lsp/view.go b/internal/lsp/view.go
index 825981f..19400d6 100644
--- a/internal/lsp/view.go
+++ b/internal/lsp/view.go
@@ -13,14 +13,21 @@
 type view struct {
 	activeFilesMu sync.Mutex
 	activeFiles   map[protocol.DocumentURI][]byte
+	config        *packages.Config
 
 	fset *token.FileSet
 }
 
 func newView() *view {
+	fset := token.NewFileSet()
 	return &view{
+		config: &packages.Config{
+			Mode:  packages.LoadSyntax,
+			Fset:  fset,
+			Tests: true,
+		},
 		activeFiles: make(map[protocol.DocumentURI][]byte),
-		fset:        token.NewFileSet(),
+		fset:        fset,
 	}
 }
 
@@ -55,14 +62,12 @@
 
 // typeCheck type-checks the package for the given package path.
 func (v *view) typeCheck(uri protocol.DocumentURI) (*packages.Package, error) {
-	cfg := &packages.Config{
-		Mode:    packages.LoadSyntax,
-		Fset:    v.fset,
-		Overlay: v.overlay(),
-		Tests:   true,
-	}
-	pkgs, err := packages.Load(cfg, fmt.Sprintf("file=%s", uriToFilename(uri)))
+	v.config.Overlay = v.overlay()
+	pkgs, err := packages.Load(v.config, fmt.Sprintf("file=%s", uriToFilename(uri)))
 	if len(pkgs) == 0 {
+		if err == nil {
+			err = fmt.Errorf("no packages found for %s", uri)
+		}
 		return nil, err
 	}
 	pkg := pkgs[0]