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)