internal/typesinternal: use Go 1.16 metadata for go/types errors

In Go 1.16 error codes as well as start and end positions are added for
go/types errors. This information is temporarily stored in unexported
fields, until we're more confident in the correctness of both the API
and the underlying data.

Read this information using reflection and, if available, use it to set
the corresponding field in compiler diagnostics. This establishes a
positive feedback loop: in most cases this should improve the gopls
diagnostic, and wherever it doesn't we can make a note and fall back on
the old heuristics for that error code.

For golang/go#42290

Change-Id: I37475189cbd14a0a5bcfde163f599c9a7b957372
Reviewed-on: https://go-review.googlesource.com/c/tools/+/268539
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Trust: Robert Findley <rfindley@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Rob Findley 2020-10-20 10:17:44 -04:00 committed by Robert Findley
parent 05664e2e8d
commit 6d2eea5430
8 changed files with 119 additions and 38 deletions

View File

@ -22,6 +22,7 @@ import (
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/typesinternal"
errors "golang.org/x/xerrors"
)
@ -31,6 +32,7 @@ func sourceError(ctx context.Context, snapshot *snapshot, pkg *pkg, e interface{
spn span.Span
err error
msg, category string
code typesinternal.ErrorCode
kind source.ErrorKind
fixes []source.SuggestedFix
related []source.RelatedInformation
@ -90,7 +92,7 @@ func sourceError(ctx context.Context, snapshot *snapshot, pkg *pkg, e interface{
if !e.Pos.IsValid() {
return nil, fmt.Errorf("invalid position for type error %v", e)
}
spn, err = typeErrorRange(snapshot, fset, pkg, e.Pos)
code, spn, err = typeErrorData(fset, pkg, e)
if err != nil {
return nil, err
}
@ -101,14 +103,14 @@ func sourceError(ctx context.Context, snapshot *snapshot, pkg *pkg, e interface{
if !perr.Pos.IsValid() {
return nil, fmt.Errorf("invalid position for type error %v", e)
}
spn, err = typeErrorRange(snapshot, fset, pkg, perr.Pos)
code, spn, err = typeErrorData(fset, pkg, e.primary)
if err != nil {
return nil, err
}
for _, s := range e.secondaries {
var x source.RelatedInformation
x.Message = s.Msg
xspn, err := typeErrorRange(snapshot, fset, pkg, s.Pos)
_, xspn, err := typeErrorData(fset, pkg, s)
if err != nil {
return nil, fmt.Errorf("invalid position for type error %v", s)
}
@ -143,7 +145,7 @@ func sourceError(ctx context.Context, snapshot *snapshot, pkg *pkg, e interface{
if err != nil {
return nil, err
}
return &source.Error{
se := &source.Error{
URI: spn.URI(),
Range: rng,
Message: msg,
@ -151,7 +153,17 @@ func sourceError(ctx context.Context, snapshot *snapshot, pkg *pkg, e interface{
Category: category,
SuggestedFixes: fixes,
Related: related,
}, nil
}
if code != 0 {
se.Code = code.String()
se.CodeHref = typesCodeHref(snapshot, code)
}
return se, nil
}
func typesCodeHref(snapshot *snapshot, code typesinternal.ErrorCode) string {
target := snapshot.View().Options().LinkTarget
return fmt.Sprintf("%s/golang.org/x/tools/internal/typesinternal#%s", target, code.String())
}
func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic) ([]source.SuggestedFix, error) {
@ -213,18 +225,29 @@ func toSourceErrorKind(kind packages.ErrorKind) source.ErrorKind {
}
}
func typeErrorRange(snapshot *snapshot, fset *token.FileSet, pkg *pkg, pos token.Pos) (span.Span, error) {
posn := fset.Position(pos)
func typeErrorData(fset *token.FileSet, pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Span, error) {
ecode, start, end, ok := typesinternal.ReadGo116ErrorData(terr)
if !ok {
start, end = terr.Pos, terr.Pos
ecode = 0
}
posn := fset.Position(start)
pgf, err := pkg.File(span.URIFromPath(posn.Filename))
if err != nil {
return span.Span{}, err
return 0, span.Span{}, err
}
return span.Range{
FileSet: fset,
Start: pos,
End: analysisinternal.TypeErrorEndPos(fset, pgf.Src, pos),
Converter: pgf.Mapper.Converter,
}.Span()
if !end.IsValid() || end == start {
end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start)
}
spn, err := parsedGoSpan(pgf, start, end)
if err != nil {
return 0, span.Span{}, err
}
return ecode, spn, nil
}
func parsedGoSpan(pgf *source.ParsedGoFile, start, end token.Pos) (span.Span, error) {
return span.FileSpan(pgf.Tok, pgf.Mapper.Converter, start, end)
}
func scannerErrorRange(snapshot *snapshot, pkg *pkg, posn token.Position) (span.Span, error) {

View File

@ -445,6 +445,8 @@ func (s *Server) storeErrorDiagnostics(ctx context.Context, snapshot source.Snap
Related: e.Related,
Severity: protocol.SeverityError,
Source: e.Category,
Code: e.Code,
CodeHref: e.CodeHref,
}
s.storeDiagnostics(snapshot, e.URI, dsource, []*source.Diagnostic{diagnostic})
}
@ -534,7 +536,7 @@ func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnost
Message: rel.Message,
})
}
reports = append(reports, protocol.Diagnostic{
pdiag := protocol.Diagnostic{
// diag.Message might start with \n or \t
Message: strings.TrimSpace(diag.Message),
Range: diag.Range,
@ -542,7 +544,14 @@ func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnost
Source: diag.Source,
Tags: diag.Tags,
RelatedInformation: related,
})
}
if diag.Code != "" {
pdiag.Code = diag.Code
}
if diag.CodeHref != "" {
pdiag.CodeDescription = &protocol.CodeDescription{Href: diag.CodeHref}
}
reports = append(reports, pdiag)
}
return reports
}

View File

@ -18,6 +18,8 @@ type Diagnostic struct {
Range protocol.Range
Message string
Source string
Code string
CodeHref string
Severity protocol.DiagnosticSeverity
Tags []protocol.DiagnosticTag

View File

@ -579,7 +579,11 @@ type Error struct {
Kind ErrorKind
Message string
Category string // only used by analysis errors so far
Related []RelatedInformation
Related []RelatedInformation
Code string
CodeHref string
// SuggestedFixes is used to generate quick fixes for a CodeAction request.
// It isn't part of the Diagnostic type.

View File

@ -18,6 +18,6 @@ type bob struct { //@item(bob, "bob", "struct{...}", "struct")
func _() {
var q int
_ = &bob{
f: q, //@diag("f", "compiler", "unknown field f in struct literal", "error")
f: q, //@diag("f: q", "compiler", "unknown field f in struct literal", "error")
}
}

View File

@ -129,18 +129,34 @@ func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string {
if w.Source != g.Source {
return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
}
if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
return summarizeDiagnostics(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
}
if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
return summarizeDiagnostics(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
}
if !rangeOverlaps(g.Range, w.Range) {
return summarizeDiagnostics(i, uri, want, got, "range %v does not overlap %v", g.Range, w.Range)
}
}
return ""
}
// rangeOverlaps reports whether r1 and r2 overlap.
func rangeOverlaps(r1, r2 protocol.Range) bool {
if inRange(r2.Start, r1) || inRange(r1.Start, r2) {
return true
}
return false
}
// inRange reports whether p is contained within [r.Start, r.End), or if p ==
// r.Start == r.End (special handling for the case where the range is a single
// point).
func inRange(p protocol.Position, r protocol.Range) bool {
if protocol.IsPoint(r) {
return protocol.ComparePosition(r.Start, p) == 0
}
if protocol.ComparePosition(r.Start, p) <= 0 && protocol.ComparePosition(p, r.End) < 0 {
return true
}
return false
}
func summarizeDiagnostics(i int, uri span.URI, want, got []*source.Diagnostic, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "diagnostics failed")

View File

@ -19,12 +19,16 @@ type Range struct {
Converter Converter
}
type FileConverter struct {
file *token.File
}
// TokenConverter is a Converter backed by a token file set and file.
// It uses the file set methods to work out the conversions, which
// makes it fast and does not require the file contents.
type TokenConverter struct {
FileConverter
fset *token.FileSet
file *token.File
}
// NewRange creates a new Range from a FileSet and two positions.
@ -40,7 +44,7 @@ func NewRange(fset *token.FileSet, start, end token.Pos) Range {
// NewTokenConverter returns an implementation of Converter backed by a
// token.File.
func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter {
return &TokenConverter{fset: fset, file: f}
return &TokenConverter{fset: fset, FileConverter: FileConverter{file: f}}
}
// NewContentConverter returns an implementation of Converter for the
@ -49,7 +53,7 @@ func NewContentConverter(filename string, content []byte) *TokenConverter {
fset := token.NewFileSet()
f := fset.AddFile(filename, -1, len(content))
f.SetLinesForContent(content)
return &TokenConverter{fset: fset, file: f}
return NewTokenConverter(fset, f)
}
// IsPoint returns true if the range represents a single point.
@ -68,17 +72,23 @@ func (r Range) Span() (Span, error) {
if f == nil {
return Span{}, fmt.Errorf("file not found in FileSet")
}
return FileSpan(f, r.Converter, r.Start, r.End)
}
// FileSpan returns a span within tok, using converter to translate between
// offsets and positions.
func FileSpan(tok *token.File, converter Converter, start, end token.Pos) (Span, error) {
var s Span
var err error
var startFilename string
startFilename, s.v.Start.Line, s.v.Start.Column, err = position(f, r.Start)
startFilename, s.v.Start.Line, s.v.Start.Column, err = position(tok, start)
if err != nil {
return Span{}, err
}
s.v.URI = URIFromPath(startFilename)
if r.End.IsValid() {
if end.IsValid() {
var endFilename string
endFilename, s.v.End.Line, s.v.End.Column, err = position(f, r.End)
endFilename, s.v.End.Line, s.v.End.Column, err = position(tok, end)
if err != nil {
return Span{}, err
}
@ -91,13 +101,13 @@ func (r Range) Span() (Span, error) {
s.v.Start.clean()
s.v.End.clean()
s.v.clean()
if r.Converter != nil {
return s.WithOffset(r.Converter)
if converter != nil {
return s.WithOffset(converter)
}
if startFilename != f.Name() {
return Span{}, fmt.Errorf("must supply Converter for file %q containing lines from %q", f.Name(), startFilename)
if startFilename != tok.Name() {
return Span{}, fmt.Errorf("must supply Converter for file %q containing lines from %q", tok.Name(), startFilename)
}
return s.WithOffset(NewTokenConverter(r.FileSet, f))
return s.WithOffset(&FileConverter{tok})
}
func position(f *token.File, pos token.Pos) (string, int, int, error) {
@ -154,12 +164,12 @@ func (s Span) Range(converter *TokenConverter) (Range, error) {
}, nil
}
func (l *TokenConverter) ToPosition(offset int) (int, int, error) {
func (l *FileConverter) ToPosition(offset int) (int, int, error) {
_, line, col, err := positionFromOffset(l.file, offset)
return line, col, err
}
func (l *TokenConverter) ToOffset(line, col int) (int, error) {
func (l *FileConverter) ToOffset(line, col int) (int, error) {
if line < 0 {
return -1, fmt.Errorf("line is not valid")
}

View File

@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package typesinternal provides access to internal go/types APIs that are not
// yet exported.
package typesinternal
import (
"go/token"
"go/types"
"reflect"
"unsafe"
@ -26,3 +29,17 @@ func SetUsesCgo(conf *types.Config) bool {
return true
}
func ReadGo116ErrorData(terr types.Error) (ErrorCode, token.Pos, token.Pos, bool) {
var data [3]int
// By coincidence all of these fields are ints, which simplifies things.
v := reflect.ValueOf(terr)
for i, name := range []string{"go116code", "go116start", "go116end"} {
f := v.FieldByName(name)
if !f.IsValid() {
return 0, 0, 0, false
}
data[i] = int(f.Int())
}
return ErrorCode(data[0]), token.Pos(data[1]), token.Pos(data[2]), true
}