internal/lsp: adding command line access to diagnostics

Change-Id: I011e337ec2bce93199cf762c09e002442ca1bd0d
Reviewed-on: https://go-review.googlesource.com/c/tools/+/167697
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cmd/check.go b/internal/lsp/cmd/check.go
new file mode 100644
index 0000000..dbc21c6
--- /dev/null
+++ b/internal/lsp/cmd/check.go
@@ -0,0 +1,110 @@
+// 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 cmd
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"go/token"
+	"io/ioutil"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+)
+
+// definition implements the definition noun for the query command.
+type check struct {
+	app *Application
+}
+
+type checkClient struct {
+	baseClient
+	diagnostics chan entry
+}
+
+type entry struct {
+	uri         span.URI
+	diagnostics []protocol.Diagnostic
+}
+
+func (c *check) Name() string      { return "check" }
+func (c *check) Usage() string     { return "<filename>" }
+func (c *check) ShortHelp() string { return "show diagnostic results for the specified file" }
+func (c *check) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), `
+Example: show the diagnostic results of this file:
+
+  $ gopls check internal/lsp/cmd/check.go
+
+	gopls check flags are:
+`)
+	f.PrintDefaults()
+}
+
+// Run performs the check on the files specified by args and prints the
+// results to stdout.
+func (c *check) Run(ctx context.Context, args ...string) error {
+	if len(args) == 0 {
+		// no files, so no results
+		return nil
+	}
+	client := &checkClient{
+		diagnostics: make(chan entry),
+	}
+	client.app = c.app
+	checking := map[span.URI][]byte{}
+	// now we ready to kick things off
+	server, err := c.app.connect(ctx, client)
+	if err != nil {
+		return err
+	}
+	for _, arg := range args {
+		uri := span.FileURI(arg)
+		content, err := ioutil.ReadFile(arg)
+		if err != nil {
+			return err
+		}
+		checking[uri] = content
+		p := &protocol.DidOpenTextDocumentParams{}
+		p.TextDocument.URI = string(uri)
+		p.TextDocument.Text = string(content)
+		if err := server.DidOpen(ctx, p); err != nil {
+			return err
+		}
+	}
+	// now wait for results
+	for entry := range client.diagnostics {
+		//TODO:timeout?
+		content, found := checking[entry.uri]
+		if !found {
+			continue
+		}
+		fset := token.NewFileSet()
+		f := fset.AddFile(string(entry.uri), -1, len(content))
+		f.SetLinesForContent(content)
+		m := protocol.NewColumnMapper(entry.uri, fset, f, content)
+		for _, d := range entry.diagnostics {
+			spn, err := m.RangeSpan(d.Range)
+			if err != nil {
+				return fmt.Errorf("Could not convert position %v for %q", d.Range, d.Message)
+			}
+			fmt.Printf("%v: %v\n", spn, d.Message)
+		}
+		delete(checking, entry.uri)
+		if len(checking) == 0 {
+			return nil
+		}
+	}
+	return fmt.Errorf("did not get all results")
+}
+
+func (c *checkClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
+	c.diagnostics <- entry{
+		uri:         span.URI(p.URI),
+		diagnostics: p.Diagnostics,
+	}
+	return nil
+}
diff --git a/internal/lsp/cmd/check_test.go b/internal/lsp/cmd/check_test.go
new file mode 100644
index 0000000..7dc292e
--- /dev/null
+++ b/internal/lsp/cmd/check_test.go
@@ -0,0 +1,78 @@
+// 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 cmd_test
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/go/packages/packagestest"
+	"golang.org/x/tools/internal/lsp/cmd"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+	"golang.org/x/tools/internal/tool"
+)
+
+type diagnostics map[string][]source.Diagnostic
+
+func (l diagnostics) collect(spn span.Span, msgSource, msg string) {
+	fname, err := spn.URI().Filename()
+	if err != nil {
+		return
+	}
+	//TODO: diagnostics with range
+	spn = span.New(spn.URI(), spn.Start(), span.Point{})
+	l[fname] = append(l[fname], source.Diagnostic{
+		Span:     spn,
+		Message:  msg,
+		Source:   msgSource,
+		Severity: source.SeverityError,
+	})
+}
+
+func (l diagnostics) test(t *testing.T, e *packagestest.Exported) {
+	count := 0
+	for fname, want := range l {
+		if len(want) == 1 && want[0].Message == "" {
+			continue
+		}
+		args := []string{"check", fname}
+		app := &cmd.Application{}
+		app.Config = *e.Config
+		out := captureStdOut(t, func() {
+			tool.Main(context.Background(), app, args)
+		})
+		// parse got into a collection of reports
+		got := map[string]struct{}{}
+		for _, l := range strings.Split(out, "\n") {
+			// parse and reprint to normalize the span
+			bits := strings.SplitN(l, ": ", 2)
+			if len(bits) == 2 {
+				spn := span.Parse(strings.TrimSpace(bits[0]))
+				spn = span.New(spn.URI(), spn.Start(), span.Point{})
+				l = fmt.Sprintf("%s: %s", spn, strings.TrimSpace(bits[1]))
+			}
+			got[l] = struct{}{}
+		}
+		for _, diag := range want {
+			expect := fmt.Sprintf("%v: %v", diag.Span, diag.Message)
+			_, found := got[expect]
+			if !found {
+				t.Errorf("missing diagnostic %q", expect)
+			} else {
+				delete(got, expect)
+			}
+		}
+		for extra, _ := range got {
+			t.Errorf("extra diagnostic %q", extra)
+		}
+		count += len(want)
+	}
+	if count != expectedDiagnosticsCount {
+		t.Errorf("got %v diagnostics expected %v", count, expectedDiagnosticsCount)
+	}
+}
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 4f2d225..afdc781 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -14,8 +14,15 @@
 	"go/ast"
 	"go/parser"
 	"go/token"
+	"net"
+	"os"
+	"strings"
 
 	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/jsonrpc2"
+	"golang.org/x/tools/internal/lsp"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/tool"
 )
 
@@ -75,6 +82,11 @@
 		tool.Main(ctx, &app.Serve, args)
 		return nil
 	}
+	if app.Config.Dir == "" {
+		if wd, err := os.Getwd(); err == nil {
+			app.Config.Dir = wd
+		}
+	}
 	app.Config.Mode = packages.LoadSyntax
 	app.Config.Tests = true
 	if app.Config.Fset == nil {
@@ -101,5 +113,77 @@
 	return []tool.Application{
 		&app.Serve,
 		&query{app: app},
+		&check{app: app},
 	}
 }
+
+func (app *Application) connect(ctx context.Context, client protocol.Client) (protocol.Server, error) {
+	var server protocol.Server
+	if app.Remote != "" {
+		conn, err := net.Dial("tcp", app.Remote)
+		if err != nil {
+			return nil, err
+		}
+		stream := jsonrpc2.NewHeaderStream(conn, conn)
+		_, server = protocol.RunClient(ctx, stream, client)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		server = lsp.NewServer(client)
+	}
+	params := &protocol.InitializeParams{}
+	params.RootURI = string(span.FileURI(app.Config.Dir))
+	if _, err := server.Initialize(ctx, params); err != nil {
+		return nil, err
+	}
+	if err := server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
+		return nil, err
+	}
+	return server, nil
+}
+
+type baseClient struct {
+	protocol.Server
+	app *Application
+}
+
+func (c *baseClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil }
+func (c *baseClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
+	return nil, nil
+}
+func (c *baseClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error { return nil }
+func (c *baseClient) Telemetry(ctx context.Context, t interface{}) error                 { return nil }
+func (c *baseClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error {
+	return nil
+}
+func (c *baseClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error {
+	return nil
+}
+func (c *baseClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) {
+	return nil, nil
+}
+func (c *baseClient) Configuration(ctx context.Context, p *protocol.ConfigurationParams) ([]interface{}, error) {
+	results := make([]interface{}, len(p.Items))
+	for i, item := range p.Items {
+		if item.Section != "gopls" {
+			continue
+		}
+		env := map[string]interface{}{}
+		for _, value := range c.app.Config.Env {
+			l := strings.SplitN(value, "=", 2)
+			if len(l) != 2 {
+				continue
+			}
+			env[l[0]] = l[1]
+		}
+		results[i] = map[string]interface{}{"env": env}
+	}
+	return results, nil
+}
+func (c *baseClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (bool, error) {
+	return false, nil
+}
+func (c *baseClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
+	return nil
+}
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index 2ffb0c2..8a56587 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -5,10 +5,6 @@
 package cmd_test
 
 import (
-	"context"
-	"go/ast"
-	"go/parser"
-	"go/token"
 	"io/ioutil"
 	"os"
 	"strings"
@@ -50,14 +46,6 @@
 	exported := packagestest.Export(t, exporter, modules)
 	defer exported.Cleanup()
 
-	// Merge the exported.Config with the view.Config.
-	cfg := *exported.Config
-	cfg.Fset = token.NewFileSet()
-	cfg.Context = context.Background()
-	cfg.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
-		return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
-	}
-
 	// Do a first pass to collect special markers for completion.
 	if err := exported.Expect(map[string]interface{}{
 		"item": func(name string, r packagestest.Range, _, _ string) {
@@ -113,34 +101,10 @@
 	})
 }
 
-type diagnostics map[span.Span][]source.Diagnostic
 type completionItems map[span.Range]*source.CompletionItem
 type completions map[span.Span][]span.Span
 type formats map[span.URI]span.Span
 
-func (l diagnostics) collect(spn span.Span, msgSource, msg string) {
-	l[spn] = append(l[spn], source.Diagnostic{
-		Span:     spn,
-		Message:  msg,
-		Source:   msgSource,
-		Severity: source.SeverityError,
-	})
-}
-
-func (l diagnostics) test(t *testing.T, e *packagestest.Exported) {
-	count := 0
-	for _, want := range l {
-		if len(want) == 1 && want[0].Message == "" {
-			continue
-		}
-		count += len(want)
-	}
-	if count != expectedDiagnosticsCount {
-		t.Errorf("got %v diagnostics expected %v", count, expectedDiagnosticsCount)
-	}
-	//TODO: add command line diagnostics tests when it works
-}
-
 func (l completionItems) collect(spn span.Range, label, detail, kind string) {
 	var k source.CompletionItemKind
 	switch kind {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 3296b4f..9f5dc34 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -25,12 +25,18 @@
 	"golang.org/x/tools/internal/span"
 )
 
+// NewServer
+func NewServer(client protocol.Client) protocol.Server {
+	return &server{
+		client:     client,
+		configured: make(chan struct{}),
+	}
+}
+
 // 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{
-		configured: make(chan struct{}),
-	}
+	s := NewServer(nil).(*server)
 	conn, client := protocol.RunServer(ctx, stream, s, opts...)
 	s.client = client
 	return conn.Wait(ctx)
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index c110c24..f6f3906 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -58,7 +58,14 @@
 	}
 	pkg := f.GetPackage(ctx)
 	if pkg == nil {
-		return nil, fmt.Errorf("diagnostics: no package found for %v", f.URI())
+		return map[span.URI][]Diagnostic{
+			uri: []Diagnostic{{
+				Source:   "LSP",
+				Span:     span.New(uri, span.Point{}, span.Point{}),
+				Message:  fmt.Sprintf("not part of a package"),
+				Severity: SeverityError,
+			}},
+		}, nil
 	}
 	// Prepare the reports we will send for this package.
 	reports := make(map[span.URI][]Diagnostic)