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>
This commit is contained in:
Ian Cottrell 2019-02-08 16:16:57 -05:00
parent 8889bfc21e
commit ab21143f23
6 changed files with 289 additions and 40 deletions

110
internal/lsp/cmd/check.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -14,8 +14,15 @@ import (
"go/ast" "go/ast"
"go/parser" "go/parser"
"go/token" "go/token"
"net"
"os"
"strings"
"golang.org/x/tools/go/packages" "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" "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) tool.Main(ctx, &app.Serve, args)
return nil return nil
} }
if app.Config.Dir == "" {
if wd, err := os.Getwd(); err == nil {
app.Config.Dir = wd
}
}
app.Config.Mode = packages.LoadSyntax app.Config.Mode = packages.LoadSyntax
app.Config.Tests = true app.Config.Tests = true
if app.Config.Fset == nil { if app.Config.Fset == nil {
@ -101,5 +113,77 @@ func (app *Application) commands() []tool.Application {
return []tool.Application{ return []tool.Application{
&app.Serve, &app.Serve,
&query{app: app}, &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
}

View File

@ -5,10 +5,6 @@
package cmd_test package cmd_test
import ( import (
"context"
"go/ast"
"go/parser"
"go/token"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
@ -50,14 +46,6 @@ func testCommandLine(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, modules) exported := packagestest.Export(t, exporter, modules)
defer exported.Cleanup() 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. // Do a first pass to collect special markers for completion.
if err := exported.Expect(map[string]interface{}{ if err := exported.Expect(map[string]interface{}{
"item": func(name string, r packagestest.Range, _, _ string) { "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 completionItems map[span.Range]*source.CompletionItem
type completions map[span.Span][]span.Span type completions map[span.Span][]span.Span
type formats map[span.URI]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) { func (l completionItems) collect(spn span.Range, label, detail, kind string) {
var k source.CompletionItemKind var k source.CompletionItemKind
switch kind { switch kind {

View File

@ -25,12 +25,18 @@ import (
"golang.org/x/tools/internal/span" "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 // RunServer starts an LSP server on the supplied stream, and waits until the
// stream is closed. // stream is closed.
func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) error { func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) error {
s := &server{ s := NewServer(nil).(*server)
configured: make(chan struct{}),
}
conn, client := protocol.RunServer(ctx, stream, s, opts...) conn, client := protocol.RunServer(ctx, stream, s, opts...)
s.client = client s.client = client
return conn.Wait(ctx) return conn.Wait(ctx)

View File

@ -58,7 +58,14 @@ func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diag
} }
pkg := f.GetPackage(ctx) pkg := f.GetPackage(ctx)
if pkg == nil { 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. // Prepare the reports we will send for this package.
reports := make(map[span.URI][]Diagnostic) reports := make(map[span.URI][]Diagnostic)