blob: 148fbda033500e3fe9e1fdc44edf02d66d6fe3f8 [file] [log] [blame]
// Copyright 2019 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 cache
import (
"context"
"fmt"
"go/scanner"
"go/token"
"go/types"
"regexp"
"strconv"
"strings"
errors "golang.org/x/xerrors"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/debug/tag"
"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"
)
func sourceDiagnostics(ctx context.Context, snapshot *snapshot, pkg *pkg, severity protocol.DiagnosticSeverity, e interface{}) ([]*source.Diagnostic, error) {
fset := snapshot.view.session.cache.fset
var (
spn span.Span
err error
msg string
code typesinternal.ErrorCode
diagSrc source.DiagnosticSource
fixes []source.SuggestedFix
related []source.RelatedInformation
)
switch e := e.(type) {
case packages.Error:
diagSrc = toDiagnosticSource(e.Kind)
var ok bool
if msg, spn, ok = parseGoListImportCycleError(ctx, snapshot, e, pkg); ok {
diagSrc = source.TypeError
break
}
if e.Pos == "" {
spn = parseGoListError(e.Msg)
// We may not have been able to parse a valid span.
if _, err := spanToRange(snapshot, pkg, spn); err != nil {
var diags []*source.Diagnostic
for _, cgf := range pkg.compiledGoFiles {
diags = append(diags, &source.Diagnostic{
URI: cgf.URI,
Severity: severity,
Source: diagSrc,
Message: msg,
})
}
return diags, nil
}
} else {
spn = span.Parse(e.Pos)
}
case *scanner.Error:
msg = e.Msg
diagSrc = source.ParseError
spn, err = scannerErrorRange(snapshot, pkg, e.Pos)
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
event.Error(ctx, "no span for scanner.Error pos", err, tag.Package.Of(pkg.ID()))
spn = span.Parse(e.Pos.String())
}
case scanner.ErrorList:
// The first parser error is likely the root cause of the problem.
if e.Len() <= 0 {
return nil, errors.Errorf("no errors in %v", e)
}
msg = e[0].Msg
diagSrc = source.ParseError
spn, err = scannerErrorRange(snapshot, pkg, e[0].Pos)
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
event.Error(ctx, "no span for scanner.Error pos", err, tag.Package.Of(pkg.ID()))
spn = span.Parse(e[0].Pos.String())
}
case types.Error:
msg = e.Msg
diagSrc = source.TypeError
if !e.Pos.IsValid() {
return nil, fmt.Errorf("invalid position for type error %v", e)
}
code, spn, err = typeErrorData(fset, pkg, e)
if err != nil {
return nil, err
}
case extendedError:
perr := e.primary
msg = perr.Msg
diagSrc = source.TypeError
if !perr.Pos.IsValid() {
return nil, fmt.Errorf("invalid position for type error %v", e)
}
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 := typeErrorData(fset, pkg, s)
if err != nil {
return nil, fmt.Errorf("invalid position for type error %v", s)
}
x.URI = xspn.URI()
rng, err := spanToRange(snapshot, pkg, xspn)
if err != nil {
return nil, err
}
x.Range = rng
related = append(related, x)
}
case *analysis.Diagnostic:
spn, err = span.NewRange(fset, e.Pos, e.End).Span()
if err != nil {
return nil, err
}
msg = e.Message
diagSrc = source.AnalyzerErrorKind(e.Category)
fixes, err = suggestedAnalysisFixes(snapshot, pkg, e)
if err != nil {
return nil, err
}
related, err = relatedInformation(snapshot, pkg, e)
if err != nil {
return nil, err
}
default:
panic(fmt.Sprintf("%T unexpected", e))
}
rng, err := spanToRange(snapshot, pkg, spn)
if err != nil {
return nil, err
}
sd := &source.Diagnostic{
URI: spn.URI(),
Range: rng,
Severity: severity,
Source: diagSrc,
Message: msg,
Related: related,
SuggestedFixes: fixes,
}
if code != 0 {
sd.Code = code.String()
sd.CodeHref = typesCodeHref(snapshot, code)
}
return []*source.Diagnostic{sd}, nil
}
func typesCodeHref(snapshot *snapshot, code typesinternal.ErrorCode) string {
target := snapshot.View().Options().LinkTarget
return fmt.Sprintf("https://%s/golang.org/x/tools/internal/typesinternal#%s", target, code.String())
}
func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic) ([]source.SuggestedFix, error) {
var fixes []source.SuggestedFix
for _, fix := range diag.SuggestedFixes {
edits := make(map[span.URI][]protocol.TextEdit)
for _, e := range fix.TextEdits {
spn, err := span.NewRange(snapshot.view.session.cache.fset, e.Pos, e.End).Span()
if err != nil {
return nil, err
}
rng, err := spanToRange(snapshot, pkg, spn)
if err != nil {
return nil, err
}
edits[spn.URI()] = append(edits[spn.URI()], protocol.TextEdit{
Range: rng,
NewText: string(e.NewText),
})
}
fixes = append(fixes, source.SuggestedFix{
Title: fix.Message,
Edits: edits,
})
}
return fixes, nil
}
func relatedInformation(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) {
var out []source.RelatedInformation
for _, related := range diag.Related {
spn, err := span.NewRange(snapshot.view.session.cache.fset, related.Pos, related.End).Span()
if err != nil {
return nil, err
}
rng, err := spanToRange(snapshot, pkg, spn)
if err != nil {
return nil, err
}
out = append(out, source.RelatedInformation{
URI: spn.URI(),
Range: rng,
Message: related.Message,
})
}
return out, nil
}
func toDiagnosticSource(kind packages.ErrorKind) source.DiagnosticSource {
switch kind {
case packages.ListError:
return source.ListError
case packages.ParseError:
return source.ParseError
case packages.TypeError:
return source.TypeError
default:
return source.UnknownError
}
}
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 0, span.Span{}, err
}
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) {
fset := snapshot.view.session.cache.fset
pgf, err := pkg.File(span.URIFromPath(posn.Filename))
if err != nil {
return span.Span{}, err
}
pos := pgf.Tok.Pos(posn.Offset)
return span.NewRange(fset, pos, pos).Span()
}
// spanToRange converts a span.Span to a protocol.Range,
// assuming that the span belongs to the package whose diagnostics are being computed.
func spanToRange(snapshot *snapshot, pkg *pkg, spn span.Span) (protocol.Range, error) {
pgf, err := pkg.File(spn.URI())
if err != nil {
return protocol.Range{}, err
}
return pgf.Mapper.Range(spn)
}
// parseGoListError attempts to parse a standard `go list` error message
// by stripping off the trailing error message.
//
// It works only on errors whose message is prefixed by colon,
// followed by a space (": "). For example:
//
// attributes.go:13:1: expected 'package', found 'type'
//
func parseGoListError(input string) span.Span {
input = strings.TrimSpace(input)
msgIndex := strings.Index(input, ": ")
if msgIndex < 0 {
return span.Parse(input)
}
return span.Parse(input[:msgIndex])
}
func parseGoListImportCycleError(ctx context.Context, snapshot *snapshot, e packages.Error, pkg *pkg) (string, span.Span, bool) {
re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`)
matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg))
if len(matches) < 3 {
return e.Msg, span.Span{}, false
}
msg := matches[1]
importList := strings.Split(matches[2], " ")
// Since the error is relative to the current package. The import that is causing
// the import cycle error is the second one in the list.
if len(importList) < 2 {
return msg, span.Span{}, false
}
// Imports have quotation marks around them.
circImp := strconv.Quote(importList[1])
for _, cgf := range pkg.compiledGoFiles {
// Search file imports for the import that is causing the import cycle.
for _, imp := range cgf.File.Imports {
if imp.Path.Value == circImp {
spn, err := span.NewRange(snapshot.view.session.cache.fset, imp.Pos(), imp.End()).Span()
if err != nil {
return msg, span.Span{}, false
}
return msg, spn, true
}
}
}
return msg, span.Span{}, false
}