mirror of
https://github.com/golang/go.git
synced 2025-05-05 23:53:05 +00:00
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:
parent
8889bfc21e
commit
ab21143f23
110
internal/lsp/cmd/check.go
Normal file
110
internal/lsp/cmd/check.go
Normal 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
|
||||
}
|
78
internal/lsp/cmd/check_test.go
Normal file
78
internal/lsp/cmd/check_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user