From ab21143f238442efbf2d6acedb69eaddbb43d088 Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Fri, 8 Feb 2019 16:16:57 -0500 Subject: [PATCH] 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 --- internal/lsp/cmd/check.go | 110 +++++++++++++++++++++++++++++ internal/lsp/cmd/check_test.go | 78 ++++++++++++++++++++ internal/lsp/cmd/cmd.go | 84 ++++++++++++++++++++++ internal/lsp/cmd/cmd_test.go | 36 ---------- internal/lsp/server.go | 12 +++- internal/lsp/source/diagnostics.go | 9 ++- 6 files changed, 289 insertions(+), 40 deletions(-) create mode 100644 internal/lsp/cmd/check.go create mode 100644 internal/lsp/cmd/check_test.go diff --git a/internal/lsp/cmd/check.go b/internal/lsp/cmd/check.go new file mode 100644 index 0000000000..dbc21c60f2 --- /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 "" } +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 0000000000..7dc292e2ae --- /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 4f2d225087..afdc781921 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -14,8 +14,15 @@ import ( "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 @@ func (app *Application) Run(ctx context.Context, args ...string) error { 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 @@ func (app *Application) commands() []tool.Application { 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 2ffb0c21c8..8a5658715a 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 @@ func testCommandLine(t *testing.T, exporter packagestest.Exporter) { 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 @@ func testCommandLine(t *testing.T, exporter packagestest.Exporter) { }) } -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 3296b4f825..9f5dc34569 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -25,12 +25,18 @@ import ( "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 c110c24633..f6f39064b1 100644 --- a/internal/lsp/source/diagnostics.go +++ b/internal/lsp/source/diagnostics.go @@ -58,7 +58,14 @@ func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diag } 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)