go/internal/lsp/source/diagnostics.go
Rebecca Stambler f344c7530c internal/lsp: add ranges to some diagnostics messages
Added a View interface to the source package, which allows for reading
of other files (in the same package or in other packages). We were
already reading files in jump to definition (to handle the lack of
column information in export data), but now we can also read files in
diagnostics, which allows us to determine the end of an identifier so
that we can report ranges in diagnostic messages.

Updates golang/go#29150

Change-Id: I7958d860dea8f41f2df88a467b5e2946bba4d1c5
Reviewed-on: https://go-review.googlesource.com/c/154742
Reviewed-by: Ian Cottrell <iancottrell@google.com>
2018-12-20 19:13:07 +00:00

130 lines
3.4 KiB
Go

// 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 (
"bytes"
"context"
"fmt"
"go/token"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
)
type Diagnostic struct {
Range
Message string
}
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 {
pos := errorPos(diag)
diagFile := v.GetFile(ToURI(pos.Filename))
diagTok, err := diagFile.GetToken()
if err != nil {
continue
}
content, err := diagFile.Read()
if err != nil {
continue
}
end, err := identifierEnd(content, pos.Line, pos.Column)
// Don't set a range if it's anything other than a type error.
if err != nil || diag.Kind != packages.TypeError {
end = 0
}
diagnostic := Diagnostic{
Range: Range{
Start: fromTokenPosition(diagTok, pos.Line, pos.Column),
End: fromTokenPosition(diagTok, pos.Line, pos.Column+end),
},
Message: diag.Msg,
}
if _, ok := reports[pos.Filename]; ok {
reports[pos.Filename] = append(reports[pos.Filename], diagnostic)
}
}
return reports, nil
}
// fromTokenPosition converts a token.Position (1-based line and column
// number) to a token.Pos (byte offset value). This requires the token.File
// to which the token.Pos belongs.
func fromTokenPosition(f *token.File, line, col int) token.Pos {
linePos := lineStart(f, line)
// TODO: This is incorrect, as pos.Column represents bytes, not characters.
// This needs to be handled to address golang.org/issue/29149.
return linePos + token.Pos(col-1)
}
func errorPos(pkgErr packages.Error) token.Position {
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
}
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
}
// identifierEnd returns the length of an identifier within a string,
// given the starting line and column numbers of the identifier.
func identifierEnd(content []byte, l, c int) (int, error) {
lines := bytes.Split(content, []byte("\n"))
if len(lines) < l {
return 0, fmt.Errorf("invalid line number: got %v, but only %v lines", l, len(lines))
}
line := lines[l-1]
if len(line) < c {
return 0, fmt.Errorf("invalid column number: got %v, but the length of the line is %v", c, len(line))
}
return bytes.IndexAny(line[c-1:], " \n,():;[]"), nil
}