mirror of
https://github.com/golang/go.git
synced 2025-05-05 15:43:04 +00:00
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:
parent
4b1f3b6b16
commit
7f27c5d70a
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
148
internal/lsp/source/diagnostics.go
Normal file
148
internal/lsp/source/diagnostics.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user