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>
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 06f39b7..39694ea 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -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
}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index ee1377e..62716cd 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -25,7 +25,8 @@
}
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 @@
},
},
Severity: protocol.SeverityError,
- Source: "LSP: Go compiler",
+ Source: "LSP",
Message: msg,
}
if _, ok := expectedDiagnostics[pos.Filename]; ok {
@@ -153,11 +154,12 @@
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
})
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index f7cf697..4c4270f 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -49,10 +49,8 @@
},
DocumentFormattingProvider: true,
DocumentRangeFormattingProvider: true,
- CompletionProvider: protocol.CompletionOptions{
- TriggerCharacters: []string{"."},
- },
- DefinitionProvider: true,
+ CompletionProvider: protocol.CompletionOptions{},
+ DefinitionProvider: true,
},
}, nil
}
@@ -119,14 +117,16 @@
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),
+ })
}
}()
}
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
new file mode 100644
index 0000000..d1ecedb
--- /dev/null
+++ b/internal/lsp/source/diagnostics.go
@@ -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
+ }
+ }
+}