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>
This commit is contained in:
parent
3571f65a7b
commit
f344c7530c
|
@ -121,6 +121,10 @@ type Range struct {
|
||||||
End token.Pos
|
End token.Pos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RangePosition struct {
|
||||||
|
Start, End token.Position
|
||||||
|
}
|
||||||
|
|
||||||
// Mark adds a new marker to the known set.
|
// Mark adds a new marker to the known set.
|
||||||
func (e *Exported) Mark(name string, r Range) {
|
func (e *Exported) Mark(name string, r Range) {
|
||||||
if e.markers == nil {
|
if e.markers == nil {
|
||||||
|
@ -173,13 +177,14 @@ func (e *Exported) getMarkers() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
noteType = reflect.TypeOf((*expect.Note)(nil))
|
noteType = reflect.TypeOf((*expect.Note)(nil))
|
||||||
identifierType = reflect.TypeOf(expect.Identifier(""))
|
identifierType = reflect.TypeOf(expect.Identifier(""))
|
||||||
posType = reflect.TypeOf(token.Pos(0))
|
posType = reflect.TypeOf(token.Pos(0))
|
||||||
positionType = reflect.TypeOf(token.Position{})
|
positionType = reflect.TypeOf(token.Position{})
|
||||||
rangeType = reflect.TypeOf(Range{})
|
rangeType = reflect.TypeOf(Range{})
|
||||||
fsetType = reflect.TypeOf((*token.FileSet)(nil))
|
rangePositionType = reflect.TypeOf(RangePosition{})
|
||||||
regexType = reflect.TypeOf((*regexp.Regexp)(nil))
|
fsetType = reflect.TypeOf((*token.FileSet)(nil))
|
||||||
|
regexType = reflect.TypeOf((*regexp.Regexp)(nil))
|
||||||
)
|
)
|
||||||
|
|
||||||
// converter converts from a marker's argument parsed from the comment to
|
// converter converts from a marker's argument parsed from the comment to
|
||||||
|
@ -234,6 +239,17 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) {
|
||||||
}
|
}
|
||||||
return reflect.ValueOf(r), remains, nil
|
return reflect.ValueOf(r), remains, nil
|
||||||
}, nil
|
}, nil
|
||||||
|
case pt == rangePositionType:
|
||||||
|
return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
|
||||||
|
r, remains, err := e.rangeConverter(n, args)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, nil, err
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(RangePosition{
|
||||||
|
Start: e.fset.Position(r.Start),
|
||||||
|
End: e.fset.Position(r.End),
|
||||||
|
}), remains, nil
|
||||||
|
}, nil
|
||||||
case pt == identifierType:
|
case pt == identifierType:
|
||||||
return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
|
return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) {
|
||||||
arg := args[0]
|
arg := args[0]
|
||||||
|
|
|
@ -34,9 +34,9 @@ func NewView(rootPath string) *View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFile returns a File for the given uri.
|
// GetFile returns a File for the given URI. It will always succeed because it
|
||||||
// It will always succeed, adding the file to the managed set if needed.
|
// adds the file to the managed set if needed.
|
||||||
func (v *View) GetFile(uri source.URI) *File {
|
func (v *View) GetFile(uri source.URI) source.File {
|
||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
f := v.getFile(uri)
|
f := v.getFile(uri)
|
||||||
v.mu.Unlock()
|
v.mu.Unlock()
|
||||||
|
|
|
@ -15,40 +15,35 @@ import (
|
||||||
|
|
||||||
func (s *server) CacheAndDiagnose(ctx context.Context, uri protocol.DocumentURI, text string) {
|
func (s *server) CacheAndDiagnose(ctx context.Context, uri protocol.DocumentURI, text string) {
|
||||||
f := s.view.GetFile(source.URI(uri))
|
f := s.view.GetFile(source.URI(uri))
|
||||||
f.SetContent([]byte(text))
|
if f, ok := f.(*cache.File); ok {
|
||||||
|
f.SetContent([]byte(text))
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
reports, err := source.Diagnostics(ctx, f)
|
reports, err := source.Diagnostics(ctx, s.view, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return // handle error?
|
return // handle error?
|
||||||
}
|
}
|
||||||
for filename, diagnostics := range reports {
|
for filename, diagnostics := range reports {
|
||||||
|
uri := source.ToURI(filename)
|
||||||
s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
|
s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
|
||||||
URI: protocol.DocumentURI(source.ToURI(filename)),
|
URI: protocol.DocumentURI(uri),
|
||||||
Diagnostics: toProtocolDiagnostics(s.view, diagnostics),
|
Diagnostics: toProtocolDiagnostics(s.view, uri, diagnostics),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func toProtocolDiagnostics(v *cache.View, diagnostics []source.Diagnostic) []protocol.Diagnostic {
|
func toProtocolDiagnostics(v *cache.View, uri source.URI, diagnostics []source.Diagnostic) []protocol.Diagnostic {
|
||||||
reports := []protocol.Diagnostic{}
|
reports := []protocol.Diagnostic{}
|
||||||
for _, diag := range diagnostics {
|
for _, diag := range diagnostics {
|
||||||
f := v.GetFile(source.ToURI(diag.Filename))
|
f := v.GetFile(uri)
|
||||||
tok, err := f.GetToken()
|
tok, err := f.GetToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue // handle error?
|
continue // handle error?
|
||||||
}
|
}
|
||||||
pos := fromTokenPosition(tok, diag.Position)
|
|
||||||
if !pos.IsValid() {
|
|
||||||
continue // handle error?
|
|
||||||
}
|
|
||||||
reports = append(reports, protocol.Diagnostic{
|
reports = append(reports, protocol.Diagnostic{
|
||||||
Message: diag.Message,
|
Message: diag.Message,
|
||||||
Range: toProtocolRange(tok, source.Range{
|
Range: toProtocolRange(tok, diag.Range),
|
||||||
Start: pos,
|
|
||||||
End: pos,
|
|
||||||
}),
|
|
||||||
Severity: protocol.SeverityError, // all diagnostics have error severity for now
|
Severity: protocol.SeverityError, // all diagnostics have error severity for now
|
||||||
Source: "LSP",
|
Source: "LSP",
|
||||||
})
|
})
|
||||||
|
@ -59,10 +54,10 @@ func toProtocolDiagnostics(v *cache.View, diagnostics []source.Diagnostic) []pro
|
||||||
func sorted(d []protocol.Diagnostic) {
|
func sorted(d []protocol.Diagnostic) {
|
||||||
sort.Slice(d, func(i int, j int) bool {
|
sort.Slice(d, func(i int, j int) bool {
|
||||||
if d[i].Range.Start.Line == d[j].Range.Start.Line {
|
if d[i].Range.Start.Line == d[j].Range.Start.Line {
|
||||||
if d[i].Range.Start.Character == d[j].Range.End.Character {
|
if d[i].Range.Start.Character == d[j].Range.Start.Character {
|
||||||
return d[i].Message < d[j].Message
|
return d[i].Message < d[j].Message
|
||||||
}
|
}
|
||||||
return d[i].Range.Start.Character < d[j].Range.End.Character
|
return d[i].Range.Start.Character < d[j].Range.Start.Character
|
||||||
}
|
}
|
||||||
return d[i].Range.Start.Line < d[j].Range.Start.Line
|
return d[i].Range.Start.Line < d[j].Range.Start.Line
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"go/token"
|
"go/token"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -36,7 +35,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
|
||||||
// We hardcode the expected number of test cases to ensure that all tests
|
// We hardcode the expected number of test cases to ensure that all tests
|
||||||
// are being executed. If a test is added, this number must be changed.
|
// are being executed. If a test is added, this number must be changed.
|
||||||
const expectedCompletionsCount = 60
|
const expectedCompletionsCount = 60
|
||||||
const expectedDiagnosticsCount = 15
|
const expectedDiagnosticsCount = 13
|
||||||
const expectedFormatCount = 3
|
const expectedFormatCount = 3
|
||||||
const expectedDefinitionsCount = 16
|
const expectedDefinitionsCount = 16
|
||||||
const expectedTypeDefinitionsCount = 2
|
const expectedTypeDefinitionsCount = 2
|
||||||
|
@ -155,47 +154,94 @@ func (d diagnostics) test(t *testing.T, exported *packagestest.Exported, v *cach
|
||||||
count := 0
|
count := 0
|
||||||
for filename, want := range d {
|
for filename, want := range d {
|
||||||
f := v.GetFile(source.ToURI(filename))
|
f := v.GetFile(source.ToURI(filename))
|
||||||
sourceDiagnostics, err := source.Diagnostics(context.Background(), f)
|
sourceDiagnostics, err := source.Diagnostics(context.Background(), v, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
got := toProtocolDiagnostics(v, sourceDiagnostics[filename])
|
got := toProtocolDiagnostics(v, source.ToURI(filename), sourceDiagnostics[filename])
|
||||||
sorted(got)
|
sorted(got)
|
||||||
if equal := reflect.DeepEqual(want, got); !equal {
|
if diff := diffDiagnostics(filename, want, got); diff != "" {
|
||||||
t.Error(diffD(filename, want, got))
|
t.Error(diff)
|
||||||
}
|
}
|
||||||
count += len(want)
|
count += len(want)
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d diagnostics) collect(pos token.Position, msg string) {
|
func (d diagnostics) collect(rng packagestest.RangePosition, msg string) {
|
||||||
if _, ok := d[pos.Filename]; !ok {
|
if rng.Start.Filename != rng.End.Filename {
|
||||||
d[pos.Filename] = []protocol.Diagnostic{}
|
return
|
||||||
|
}
|
||||||
|
filename := rng.Start.Filename
|
||||||
|
if _, ok := d[filename]; !ok {
|
||||||
|
d[filename] = []protocol.Diagnostic{}
|
||||||
}
|
}
|
||||||
// If a file has an empty diagnostics, mark that and return. This allows us
|
// If a file has an empty diagnostics, mark that and return. This allows us
|
||||||
// to avoid testing diagnostics in files that may have a lot of them.
|
// to avoid testing diagnostics in files that may have a lot of them.
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
line := float64(pos.Line - 1)
|
|
||||||
col := float64(pos.Column - 1)
|
|
||||||
want := protocol.Diagnostic{
|
want := protocol.Diagnostic{
|
||||||
Range: protocol.Range{
|
Range: protocol.Range{
|
||||||
Start: protocol.Position{
|
Start: protocol.Position{
|
||||||
Line: line,
|
Line: float64(rng.Start.Line - 1),
|
||||||
Character: col,
|
Character: float64(rng.Start.Column - 1),
|
||||||
},
|
},
|
||||||
End: protocol.Position{
|
End: protocol.Position{
|
||||||
Line: line,
|
Line: float64(rng.End.Line - 1),
|
||||||
Character: col,
|
Character: float64(rng.End.Column - 1),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Severity: protocol.SeverityError,
|
Severity: protocol.SeverityError,
|
||||||
Source: "LSP",
|
Source: "LSP",
|
||||||
Message: msg,
|
Message: msg,
|
||||||
}
|
}
|
||||||
d[pos.Filename] = append(d[pos.Filename], want)
|
d[filename] = append(d[filename], want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// diffDiagnostics prints the diff between expected and actual diagnostics test
|
||||||
|
// results.
|
||||||
|
func diffDiagnostics(filename string, want, got []protocol.Diagnostic) string {
|
||||||
|
if len(got) != len(want) {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
for i, w := range want {
|
||||||
|
g := got[i]
|
||||||
|
if w.Message != g.Message {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
if w.Range.Start != g.Range.Start {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
// Special case for diagnostics on parse errors.
|
||||||
|
if strings.Contains(filename, "noparse") {
|
||||||
|
if g.Range.Start != g.Range.End || w.Range.Start != g.Range.End {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if w.Range.End != g.Range.End {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.Severity != g.Severity {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
if w.Source != g.Source {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
Failed:
|
||||||
|
msg := &bytes.Buffer{}
|
||||||
|
fmt.Fprintf(msg, "diagnostics failed for %s:\nexpected:\n", filename)
|
||||||
|
for _, d := range want {
|
||||||
|
fmt.Fprintf(msg, " %v\n", d)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(msg, "got:\n")
|
||||||
|
for _, d := range got {
|
||||||
|
fmt.Fprintf(msg, " %v\n", d)
|
||||||
|
}
|
||||||
|
return msg.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c completions) test(t *testing.T, exported *packagestest.Exported, s *server, items completionItems) {
|
func (c completions) test(t *testing.T, exported *packagestest.Exported, s *server, items completionItems) {
|
||||||
|
@ -240,7 +286,7 @@ func (c completions) test(t *testing.T, exported *packagestest.Exported, s *serv
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("completion failed for %s:%v:%v: %v", filepath.Base(src.Filename), src.Line, src.Column, err)
|
t.Fatalf("completion failed for %s:%v:%v: %v", filepath.Base(src.Filename), src.Line, src.Column, err)
|
||||||
}
|
}
|
||||||
if diff := diffC(src, want, got); diff != "" {
|
if diff := diffCompletionItems(src, want, got); diff != "" {
|
||||||
t.Errorf(diff)
|
t.Errorf(diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,6 +325,38 @@ func (i completionItems) collect(pos token.Pos, label, detail, kind string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// diffCompletionItems prints the diff between expected and actual completion
|
||||||
|
// test results.
|
||||||
|
func diffCompletionItems(pos token.Position, want, got []protocol.CompletionItem) string {
|
||||||
|
if len(got) != len(want) {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
for i, w := range want {
|
||||||
|
g := got[i]
|
||||||
|
if w.Label != g.Label {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
if w.Detail != g.Detail {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
if w.Kind != g.Kind {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
Failed:
|
||||||
|
msg := &bytes.Buffer{}
|
||||||
|
fmt.Fprintf(msg, "completion failed for %s:%v:%v:\nexpected:\n", filepath.Base(pos.Filename), pos.Line, pos.Column)
|
||||||
|
for _, d := range want {
|
||||||
|
fmt.Fprintf(msg, " %v\n", d)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(msg, "got:\n")
|
||||||
|
for _, d := range got {
|
||||||
|
fmt.Fprintf(msg, " %v\n", d)
|
||||||
|
}
|
||||||
|
return msg.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (f formats) test(t *testing.T, s *server) {
|
func (f formats) test(t *testing.T, s *server) {
|
||||||
for filename, gofmted := range f {
|
for filename, gofmted := range f {
|
||||||
edits, err := s.Formatting(context.Background(), &protocol.DocumentFormattingParams{
|
edits, err := s.Formatting(context.Background(), &protocol.DocumentFormattingParams{
|
||||||
|
@ -341,48 +419,3 @@ func (d definitions) collect(fset *token.FileSet, src, target packagestest.Range
|
||||||
tLoc := toProtocolLocation(fset, tRange)
|
tLoc := toProtocolLocation(fset, tRange)
|
||||||
d[sLoc] = tLoc
|
d[sLoc] = tLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
// diffD prints the diff between expected and actual diagnostics test results.
|
|
||||||
func diffD(filename string, want, got []protocol.Diagnostic) string {
|
|
||||||
msg := &bytes.Buffer{}
|
|
||||||
fmt.Fprintf(msg, "diagnostics failed for %s:\nexpected:\n", filename)
|
|
||||||
for _, d := range want {
|
|
||||||
fmt.Fprintf(msg, " %v\n", d)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(msg, "got:\n")
|
|
||||||
for _, d := range got {
|
|
||||||
fmt.Fprintf(msg, " %v\n", d)
|
|
||||||
}
|
|
||||||
return msg.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// diffC prints the diff between expected and actual completion test results.
|
|
||||||
func diffC(pos token.Position, want, got []protocol.CompletionItem) string {
|
|
||||||
if len(got) != len(want) {
|
|
||||||
goto Failed
|
|
||||||
}
|
|
||||||
for i, w := range want {
|
|
||||||
g := got[i]
|
|
||||||
if w.Label != g.Label {
|
|
||||||
goto Failed
|
|
||||||
}
|
|
||||||
if w.Detail != g.Detail {
|
|
||||||
goto Failed
|
|
||||||
}
|
|
||||||
if w.Kind != g.Kind {
|
|
||||||
goto Failed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
Failed:
|
|
||||||
msg := &bytes.Buffer{}
|
|
||||||
fmt.Fprintf(msg, "completion failed for %s:%v:%v:\nexpected:\n", filepath.Base(pos.Filename), pos.Line, pos.Column)
|
|
||||||
for _, d := range want {
|
|
||||||
fmt.Fprintf(msg, " %v\n", d)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(msg, "got:\n")
|
|
||||||
for _, d := range got {
|
|
||||||
fmt.Fprintf(msg, " %v\n", d)
|
|
||||||
}
|
|
||||||
return msg.String()
|
|
||||||
}
|
|
||||||
|
|
|
@ -143,12 +143,14 @@ func (s *server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocume
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error {
|
func (s *server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error {
|
||||||
// TODO(rstambler): Should we clear the cache here?
|
|
||||||
return nil // ignore
|
return nil // ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
|
func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
|
||||||
s.view.GetFile(source.URI(params.TextDocument.URI)).SetContent(nil)
|
f := s.view.GetFile(source.URI(params.TextDocument.URI))
|
||||||
|
if f, ok := f.(*cache.File); ok {
|
||||||
|
f.SetContent(nil)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,7 +216,7 @@ func (s *server) Definition(ctx context.Context, params *protocol.TextDocumentPo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pos := fromProtocolPosition(tok, params.Position)
|
pos := fromProtocolPosition(tok, params.Position)
|
||||||
r, err := source.Definition(ctx, f, pos)
|
r, err := source.Definition(ctx, s.view, f, pos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -228,7 +230,7 @@ func (s *server) TypeDefinition(ctx context.Context, params *protocol.TextDocume
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pos := fromProtocolPosition(tok, params.Position)
|
pos := fromProtocolPosition(tok, params.Position)
|
||||||
r, err := source.TypeDefinition(ctx, f, pos)
|
r, err := source.TypeDefinition(ctx, s.view, f, pos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,11 @@ import (
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/token"
|
"go/token"
|
||||||
"go/types"
|
"go/types"
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"golang.org/x/tools/go/ast/astutil"
|
"golang.org/x/tools/go/ast/astutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Definition(ctx context.Context, f File, pos token.Pos) (Range, error) {
|
func Definition(ctx context.Context, v View, f File, pos token.Pos) (Range, error) {
|
||||||
fAST, err := f.GetAST()
|
fAST, err := f.GetAST()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Range{}, err
|
return Range{}, err
|
||||||
|
@ -49,10 +48,10 @@ func Definition(ctx context.Context, f File, pos token.Pos) (Range, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Range{}, err
|
return Range{}, err
|
||||||
}
|
}
|
||||||
return objToRange(fset, obj), nil
|
return objToRange(v, fset, obj), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TypeDefinition(ctx context.Context, f File, pos token.Pos) (Range, error) {
|
func TypeDefinition(ctx context.Context, v View, f File, pos token.Pos) (Range, error) {
|
||||||
fAST, err := f.GetAST()
|
fAST, err := f.GetAST()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Range{}, err
|
return Range{}, err
|
||||||
|
@ -80,7 +79,7 @@ func TypeDefinition(ctx context.Context, f File, pos token.Pos) (Range, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Range{}, err
|
return Range{}, err
|
||||||
}
|
}
|
||||||
return objToRange(fset, obj), nil
|
return objToRange(v, fset, obj), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func typeToObject(typ types.Type) (obj types.Object) {
|
func typeToObject(typ types.Type) (obj types.Object) {
|
||||||
|
@ -138,33 +137,43 @@ func checkIdentifier(f *ast.File, pos token.Pos) (ident, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func objToRange(fSet *token.FileSet, obj types.Object) Range {
|
func objToRange(v View, fset *token.FileSet, obj types.Object) Range {
|
||||||
p := obj.Pos()
|
p := obj.Pos()
|
||||||
f := fSet.File(p)
|
f := fset.File(p)
|
||||||
pos := f.Position(p)
|
pos := f.Position(p)
|
||||||
if pos.Column == 1 {
|
if pos.Column == 1 {
|
||||||
// Column is 1, so we probably do not have full position information
|
// We do not have full position information because exportdata does not
|
||||||
// Currently exportdata does not store the column.
|
// store the column. For now, we attempt to read the original source
|
||||||
// For now we attempt to read the original source and find the identifier
|
// and find the identifier within the line. If we find it, we patch the
|
||||||
// within the line. If we find it we patch the column to match its offset.
|
// column to match its offset.
|
||||||
// TODO: we have probably already added the full data for the file to the
|
//
|
||||||
// fileset, we ought to track it rather than adding it over and over again
|
// TODO: If we parse from source, we will never need this hack.
|
||||||
// TODO: if we parse from source, we will never need this hack
|
f := v.GetFile(ToURI(pos.Filename))
|
||||||
if src, err := ioutil.ReadFile(pos.Filename); err == nil {
|
tok, err := f.GetToken()
|
||||||
newF := fSet.AddFile(pos.Filename, -1, len(src))
|
if err != nil {
|
||||||
newF.SetLinesForContent(src)
|
goto Return
|
||||||
lineStart := lineStart(newF, pos.Line)
|
|
||||||
offset := newF.Offset(lineStart)
|
|
||||||
col := bytes.Index(src[offset:], []byte(obj.Name()))
|
|
||||||
p = newF.Pos(offset + col)
|
|
||||||
}
|
}
|
||||||
|
src, err := f.Read()
|
||||||
|
if err != nil {
|
||||||
|
goto Return
|
||||||
|
}
|
||||||
|
start := lineStart(tok, pos.Line)
|
||||||
|
offset := tok.Offset(start)
|
||||||
|
col := bytes.Index(src[offset:], []byte(obj.Name()))
|
||||||
|
p = tok.Pos(offset + col)
|
||||||
}
|
}
|
||||||
|
Return:
|
||||||
return Range{
|
return Range{
|
||||||
Start: p,
|
Start: p,
|
||||||
End: p + token.Pos(len([]byte(obj.Name()))), // TODO: use real range of obj
|
End: p + token.Pos(identifierLen(obj.Name())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This needs to be fixed to address golang.org/issue/29149.
|
||||||
|
func identifierLen(ident string) int {
|
||||||
|
return len([]byte(ident))
|
||||||
|
}
|
||||||
|
|
||||||
// this functionality was borrowed from the analysisutil package
|
// this functionality was borrowed from the analysisutil package
|
||||||
func lineStart(f *token.File, line int) token.Pos {
|
func lineStart(f *token.File, line int) token.Pos {
|
||||||
// Use binary search to find the start offset of this line.
|
// Use binary search to find the start offset of this line.
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"go/token"
|
"go/token"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -14,11 +16,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Diagnostic struct {
|
type Diagnostic struct {
|
||||||
token.Position
|
Range
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Diagnostics(ctx context.Context, f File) (map[string][]Diagnostic, error) {
|
func Diagnostics(ctx context.Context, v View, f File) (map[string][]Diagnostic, error) {
|
||||||
pkg, err := f.GetPackage()
|
pkg, err := f.GetPackage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -47,9 +49,26 @@ func Diagnostics(ctx context.Context, f File) (map[string][]Diagnostic, error) {
|
||||||
}
|
}
|
||||||
for _, diag := range diags {
|
for _, diag := range diags {
|
||||||
pos := errorPos(diag)
|
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{
|
diagnostic := Diagnostic{
|
||||||
Position: pos,
|
Range: Range{
|
||||||
Message: diag.Msg,
|
Start: fromTokenPosition(diagTok, pos.Line, pos.Column),
|
||||||
|
End: fromTokenPosition(diagTok, pos.Line, pos.Column+end),
|
||||||
|
},
|
||||||
|
Message: diag.Msg,
|
||||||
}
|
}
|
||||||
if _, ok := reports[pos.Filename]; ok {
|
if _, ok := reports[pos.Filename]; ok {
|
||||||
reports[pos.Filename] = append(reports[pos.Filename], diagnostic)
|
reports[pos.Filename] = append(reports[pos.Filename], diagnostic)
|
||||||
|
@ -58,12 +77,14 @@ func Diagnostics(ctx context.Context, f File) (map[string][]Diagnostic, error) {
|
||||||
return reports, nil
|
return reports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromTokenPosition converts a token.Position (1-based line and column
|
// fromTokenPosition converts a token.Position (1-based line and column
|
||||||
// number) to a token.Pos (byte offset value).
|
// number) to a token.Pos (byte offset value). This requires the token.File
|
||||||
// It requires the token file the pos belongs to in order to do this.
|
// to which the token.Pos belongs.
|
||||||
func FromTokenPosition(f *token.File, pos token.Position) token.Pos {
|
func fromTokenPosition(f *token.File, line, col int) token.Pos {
|
||||||
line := lineStart(f, pos.Line)
|
linePos := lineStart(f, line)
|
||||||
return line + token.Pos(pos.Column-1) // TODO: this is wrong, bytes not characters
|
// 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 {
|
func errorPos(pkgErr packages.Error) token.Position {
|
||||||
|
@ -92,3 +113,17 @@ func chop(text string) (remainder string, value int, ok bool) {
|
||||||
}
|
}
|
||||||
return text[:i], int(v), true
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ func toURI(path string) *url.URL {
|
||||||
// Handle standard library paths that contain the literal "$GOROOT".
|
// Handle standard library paths that contain the literal "$GOROOT".
|
||||||
// TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT.
|
// TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT.
|
||||||
const prefix = "$GOROOT"
|
const prefix = "$GOROOT"
|
||||||
if strings.EqualFold(prefix, path[:len(prefix)]) {
|
if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) {
|
||||||
suffix := path[len(prefix):]
|
suffix := path[len(prefix):]
|
||||||
path = runtime.GOROOT() + suffix
|
path = runtime.GOROOT() + suffix
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,14 @@ import (
|
||||||
"golang.org/x/tools/go/packages"
|
"golang.org/x/tools/go/packages"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// View abstracts the underlying architecture of the package using the source
|
||||||
|
// package. The view provides access to files and their contents, so the source
|
||||||
|
// package does not directly access the file system.
|
||||||
|
type View interface {
|
||||||
|
// Consider adding an error to this method, if users require it.
|
||||||
|
GetFile(uri URI) File
|
||||||
|
}
|
||||||
|
|
||||||
// File represents a Go source file that has been type-checked. It is the input
|
// File represents a Go source file that has been type-checked. It is the input
|
||||||
// to most of the exported functions in this package, as it wraps up the
|
// to most of the exported functions in this package, as it wraps up the
|
||||||
// building blocks for most queries. Users of the source package can abstract
|
// building blocks for most queries. Users of the source package can abstract
|
||||||
|
@ -20,6 +28,7 @@ type File interface {
|
||||||
GetFileSet() (*token.FileSet, error)
|
GetFileSet() (*token.FileSet, error)
|
||||||
GetPackage() (*packages.Package, error)
|
GetPackage() (*packages.Package, error)
|
||||||
GetToken() (*token.File, error)
|
GetToken() (*token.File, error)
|
||||||
|
Read() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range represents a start and end position.
|
// Range represents a start and end position.
|
|
@ -7,5 +7,3 @@ func unclosedIf() {
|
||||||
var myUnclosedIf string //@myUnclosedIf
|
var myUnclosedIf string //@myUnclosedIf
|
||||||
fmt.Printf("s = %v\n", myUnclosedIf) //@godef("my", myUnclosedIf)
|
fmt.Printf("s = %v\n", myUnclosedIf) //@godef("my", myUnclosedIf)
|
||||||
}
|
}
|
||||||
//@diag(EOF, "expected ';', found 'EOF'")
|
|
||||||
//@diag(EOF, "expected '}', found 'EOF'")
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
package self
|
package self
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"golang.org/x/tools/internal/lsp/self" //@diag("\"", "could not import golang.org/x/tools/internal/lsp/self (import cycle: [golang.org/x/tools/internal/lsp/self])")
|
"golang.org/x/tools/internal/lsp/self" //@diag("\"golang.org/x/tools/internal/lsp/self\"", "could not import golang.org/x/tools/internal/lsp/self (import cycle: [golang.org/x/tools/internal/lsp/self])")
|
||||||
)
|
)
|
||||||
|
|
||||||
func Hello() {}
|
func Hello() {}
|
||||||
|
|
Loading…
Reference in New Issue