| // 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 |
| } |
| } |
| } |