internal/lsp: move diagnostics logic to source directory

Change-Id: I6bea7a76501e852bbf381eb5dbc79217e1ad10ac
Reviewed-on: https://go-review.googlesource.com/c/148889
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Rebecca Stambler 2018-11-12 14:15:47 -05:00
parent 4b1f3b6b16
commit 7f27c5d70a
4 changed files with 188 additions and 98 deletions

View File

@ -5,94 +5,34 @@
package lsp
import (
"fmt"
"go/token"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
)
func diagnostics(v *source.View, uri source.URI) (map[string][]protocol.Diagnostic, error) {
pkg, err := v.GetFile(uri).GetPackage()
if err != nil {
return nil, err
func toProtocolDiagnostics(v *source.View, diagnostics []source.Diagnostic) []protocol.Diagnostic {
reports := []protocol.Diagnostic{}
for _, diag := range diagnostics {
tok := v.Config.Fset.File(diag.Range.Start)
reports = append(reports, protocol.Diagnostic{
Message: diag.Message,
Range: toProtocolRange(tok, diag.Range),
Severity: toProtocolSeverity(diag.Severity),
Source: "LSP",
})
}
if pkg == nil {
return nil, fmt.Errorf("package for %v not found", uri)
}
reports := make(map[string][]protocol.Diagnostic)
for _, filename := range pkg.GoFiles {
reports[filename] = []protocol.Diagnostic{}
}
var parseErrors, typeErrors []packages.Error
for _, err := range pkg.Errors {
switch err.Kind {
case packages.ParseError:
parseErrors = append(parseErrors, err)
case packages.TypeError:
typeErrors = append(typeErrors, err)
default:
// ignore other types of errors
continue
}
}
// Don't report type errors if there are parse errors.
errors := typeErrors
if len(parseErrors) > 0 {
errors = parseErrors
}
for _, err := range errors {
pos := parseErrorPos(err)
line := float64(pos.Line) - 1
col := float64(pos.Column) - 1
diagnostic := protocol.Diagnostic{
// TODO(rstambler): Add support for diagnostic ranges.
Range: protocol.Range{
Start: protocol.Position{
Line: line,
Character: col,
},
End: protocol.Position{
Line: line,
Character: col,
},
},
Severity: protocol.SeverityError,
Source: "LSP: Go compiler",
Message: err.Msg,
}
if _, ok := reports[pos.Filename]; ok {
reports[pos.Filename] = append(reports[pos.Filename], diagnostic)
}
}
return reports, nil
return reports
}
func parseErrorPos(pkgErr packages.Error) (pos token.Position) {
remainder1, first, hasLine := chop(pkgErr.Pos)
remainder2, second, hasColumn := chop(remainder1)
if hasLine && hasColumn {
pos.Filename = remainder2
pos.Line = second
pos.Column = first
} else if hasLine {
pos.Filename = remainder1
pos.Line = first
func toProtocolSeverity(severity source.DiagnosticSeverity) protocol.DiagnosticSeverity {
switch severity {
case source.SeverityError:
return protocol.SeverityError
case source.SeverityWarning:
return protocol.SeverityWarning
case source.SeverityHint:
return protocol.SeverityHint
case source.SeverityInformation:
return protocol.SeverityInformation
}
return pos
}
func chop(text string) (remainder string, value int, ok bool) {
i := strings.LastIndex(text, ":")
if i < 0 {
return text, 0, false
}
v, err := strconv.ParseInt(text[i+1:], 10, 64)
if err != nil {
return text, 0, false
}
return text[:i], int(v), true
return protocol.SeverityError // default
}

View File

@ -25,7 +25,8 @@ func TestLSP(t *testing.T) {
}
func testLSP(t *testing.T, exporter packagestest.Exporter) {
dir := "testdata"
const dir = "testdata"
files := packagestest.MustCopyFileTree(dir)
subdirs, err := ioutil.ReadDir(dir)
if err != nil {
@ -95,7 +96,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
},
},
Severity: protocol.SeverityError,
Source: "LSP: Go compiler",
Source: "LSP",
Message: msg,
}
if _, ok := expectedDiagnostics[pos.Filename]; ok {
@ -153,11 +154,12 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
func testDiagnostics(t *testing.T, v *source.View, pkgs []*packages.Package, wants map[string][]protocol.Diagnostic) {
for _, pkg := range pkgs {
for _, filename := range pkg.GoFiles {
diagnostics, err := diagnostics(v, source.ToURI(filename))
f := v.GetFile(source.ToURI(filename))
diagnostics, err := source.Diagnostics(context.Background(), v, f)
if err != nil {
t.Fatal(err)
}
got := diagnostics[filename]
got := toProtocolDiagnostics(v, diagnostics[filename])
sort.Slice(got, func(i int, j int) bool {
return got[i].Range.Start.Line < got[j].Range.Start.Line
})

View File

@ -49,10 +49,8 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara
},
DocumentFormattingProvider: true,
DocumentRangeFormattingProvider: true,
CompletionProvider: protocol.CompletionOptions{
TriggerCharacters: []string{"."},
},
DefinitionProvider: true,
CompletionProvider: protocol.CompletionOptions{},
DefinitionProvider: true,
},
}, nil
}
@ -119,14 +117,16 @@ func (s *server) cacheAndDiagnoseFile(ctx context.Context, uri protocol.Document
f := s.view.GetFile(source.URI(uri))
f.SetContent([]byte(text))
go func() {
reports, err := diagnostics(s.view, f.URI)
if err == nil {
for filename, diagnostics := range reports {
s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
URI: protocol.DocumentURI(source.ToURI(filename)),
Diagnostics: diagnostics,
})
}
f := s.view.GetFile(source.URI(uri))
reports, err := source.Diagnostics(ctx, s.view, f)
if err != nil {
return // handle error?
}
for filename, diagnostics := range reports {
s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
URI: protocol.DocumentURI(source.ToURI(filename)),
Diagnostics: toProtocolDiagnostics(s.view, diagnostics),
})
}
}()
}

View File

@ -0,0 +1,148 @@
// Copyright 2018 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 source
import (
"context"
"go/token"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
)
type Diagnostic struct {
Range Range
Severity DiagnosticSeverity
Message string
}
type DiagnosticSeverity int
const (
SeverityError DiagnosticSeverity = iota
SeverityWarning
SeverityHint
SeverityInformation
)
func Diagnostics(ctx context.Context, v *View, f *File) (map[string][]Diagnostic, error) {
pkg, err := f.GetPackage()
if err != nil {
return nil, err
}
// Prepare the reports we will send for this package.
reports := make(map[string][]Diagnostic)
for _, filename := range pkg.GoFiles {
reports[filename] = []Diagnostic{}
}
var parseErrors, typeErrors []packages.Error
for _, err := range pkg.Errors {
switch err.Kind {
case packages.ParseError:
parseErrors = append(parseErrors, err)
case packages.TypeError:
typeErrors = append(typeErrors, err)
default:
// ignore other types of errors
continue
}
}
// Don't report type errors if there are parse errors.
diags := typeErrors
if len(parseErrors) > 0 {
diags = parseErrors
}
for _, diag := range diags {
filename, start := v.errorPos(diag)
// TODO(rstambler): Add support for diagnostic ranges.
end := start
diagnostic := Diagnostic{
Range: Range{
Start: start,
End: end,
},
Message: diag.Msg,
Severity: SeverityError,
}
if _, ok := reports[filename]; ok {
reports[filename] = append(reports[filename], diagnostic)
}
}
return reports, nil
}
func (v *View) errorPos(pkgErr packages.Error) (string, token.Pos) {
remainder1, first, hasLine := chop(pkgErr.Pos)
remainder2, second, hasColumn := chop(remainder1)
var pos token.Position
if hasLine && hasColumn {
pos.Filename = remainder2
pos.Line = second
pos.Column = first
} else if hasLine {
pos.Filename = remainder1
pos.Line = first
}
f := v.GetFile(ToURI(pos.Filename))
if f == nil {
return "", token.NoPos
}
tok, err := f.GetToken()
if err != nil {
return "", token.NoPos
}
return pos.Filename, fromTokenPosition(tok, pos)
}
func chop(text string) (remainder string, value int, ok bool) {
i := strings.LastIndex(text, ":")
if i < 0 {
return text, 0, false
}
v, err := strconv.ParseInt(text[i+1:], 10, 64)
if err != nil {
return text, 0, false
}
return text[:i], int(v), true
}
// fromTokenPosition converts a token.Position (1-based line and column
// number) to a token.Pos (byte offset value).
// It requires the token file the pos belongs to in order to do this.
func fromTokenPosition(f *token.File, pos token.Position) token.Pos {
line := lineStart(f, pos.Line)
return line + token.Pos(pos.Column-1) // TODO: this is wrong, bytes not characters
}
// this functionality was borrowed from the analysisutil package
func lineStart(f *token.File, line int) token.Pos {
// Use binary search to find the start offset of this line.
//
// TODO(adonovan): eventually replace this function with the
// simpler and more efficient (*go/token.File).LineStart, added
// in go1.12.
min := 0 // inclusive
max := f.Size() // exclusive
for {
offset := (min + max) / 2
pos := f.Pos(offset)
posn := f.Position(pos)
if posn.Line == line {
return pos - (token.Pos(posn.Column) - 1)
}
if min+1 >= max {
return token.NoPos
}
if posn.Line < line {
min = offset
} else {
max = offset
}
}
}