mirror of https://github.com/golang/go.git
389 lines
11 KiB
Go
389 lines
11 KiB
Go
// 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 (
|
|
"fmt"
|
|
"go/scanner"
|
|
"go/token"
|
|
"go/types"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/packages"
|
|
"golang.org/x/tools/internal/analysisinternal"
|
|
"golang.org/x/tools/internal/lsp/command"
|
|
"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"
|
|
)
|
|
|
|
func goPackagesErrorDiagnostics(snapshot *snapshot, pkg *pkg, e packages.Error) ([]*source.Diagnostic, error) {
|
|
if msg, spn, ok := parseGoListImportCycleError(snapshot, e, pkg); ok {
|
|
rng, err := spanToRange(pkg, spn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []*source.Diagnostic{{
|
|
URI: spn.URI(),
|
|
Range: rng,
|
|
Severity: protocol.SeverityError,
|
|
Source: source.ListError,
|
|
Message: msg,
|
|
}}, nil
|
|
}
|
|
|
|
var spn span.Span
|
|
if e.Pos == "" {
|
|
spn = parseGoListError(e.Msg, pkg.m.Config.Dir)
|
|
// We may not have been able to parse a valid span. Apply the errors to all files.
|
|
if _, err := spanToRange(pkg, spn); err != nil {
|
|
var diags []*source.Diagnostic
|
|
for _, cgf := range pkg.compiledGoFiles {
|
|
diags = append(diags, &source.Diagnostic{
|
|
URI: cgf.URI,
|
|
Severity: protocol.SeverityError,
|
|
Source: source.ListError,
|
|
Message: e.Msg,
|
|
})
|
|
}
|
|
return diags, nil
|
|
}
|
|
} else {
|
|
spn = span.ParseInDir(e.Pos, pkg.m.Config.Dir)
|
|
}
|
|
|
|
rng, err := spanToRange(pkg, spn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []*source.Diagnostic{{
|
|
URI: spn.URI(),
|
|
Range: rng,
|
|
Severity: protocol.SeverityError,
|
|
Source: source.ListError,
|
|
Message: e.Msg,
|
|
}}, nil
|
|
}
|
|
|
|
func parseErrorDiagnostics(snapshot *snapshot, pkg *pkg, errList scanner.ErrorList) ([]*source.Diagnostic, error) {
|
|
// The first parser error is likely the root cause of the problem.
|
|
if errList.Len() <= 0 {
|
|
return nil, errors.Errorf("no errors in %v", errList)
|
|
}
|
|
e := errList[0]
|
|
pgf, err := pkg.File(span.URIFromPath(e.Pos.Filename))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pos := pgf.Tok.Pos(e.Pos.Offset)
|
|
spn, err := span.NewRange(snapshot.FileSet(), pos, pos).Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := spanToRange(pkg, spn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []*source.Diagnostic{{
|
|
URI: spn.URI(),
|
|
Range: rng,
|
|
Severity: protocol.SeverityError,
|
|
Source: source.ParseError,
|
|
Message: e.Msg,
|
|
}}, nil
|
|
}
|
|
|
|
var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
|
|
|
|
func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*source.Diagnostic, error) {
|
|
code, spn, err := typeErrorData(snapshot.FileSet(), pkg, e.primary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := spanToRange(pkg, spn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
diag := &source.Diagnostic{
|
|
URI: spn.URI(),
|
|
Range: rng,
|
|
Severity: protocol.SeverityError,
|
|
Source: source.TypeError,
|
|
Message: e.primary.Msg,
|
|
}
|
|
if code != 0 {
|
|
diag.Code = code.String()
|
|
diag.CodeHref = typesCodeHref(snapshot, code)
|
|
}
|
|
|
|
for _, secondary := range e.secondaries {
|
|
_, secondarySpan, err := typeErrorData(snapshot.FileSet(), pkg, secondary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := spanToRange(pkg, secondarySpan)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
diag.Related = append(diag.Related, source.RelatedInformation{
|
|
URI: secondarySpan.URI(),
|
|
Range: rng,
|
|
Message: secondary.Msg,
|
|
})
|
|
}
|
|
|
|
if match := importErrorRe.FindStringSubmatch(e.primary.Msg); match != nil {
|
|
diag.SuggestedFixes, err = goGetQuickFixes(snapshot, spn.URI(), match[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return []*source.Diagnostic{diag}, nil
|
|
}
|
|
|
|
func goGetQuickFixes(snapshot *snapshot, uri span.URI, pkg string) ([]source.SuggestedFix, error) {
|
|
// Go get only supports module mode for now.
|
|
if snapshot.workspaceMode()&moduleMode == 0 {
|
|
return nil, nil
|
|
}
|
|
title := fmt.Sprintf("go get package %v", pkg)
|
|
cmd, err := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{
|
|
URI: protocol.URIFromSpanURI(uri),
|
|
AddRequire: true,
|
|
Pkg: pkg,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, nil
|
|
}
|
|
|
|
func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Analyzer, e *analysis.Diagnostic) ([]*source.Diagnostic, error) {
|
|
var srcAnalyzer *source.Analyzer
|
|
// Find the analyzer that generated this diagnostic.
|
|
for _, sa := range source.EnabledAnalyzers(snapshot) {
|
|
if a == sa.Analyzer {
|
|
srcAnalyzer = sa
|
|
break
|
|
}
|
|
}
|
|
|
|
spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := spanToRange(pkg, spn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kinds := srcAnalyzer.ActionKind
|
|
if len(srcAnalyzer.ActionKind) == 0 {
|
|
kinds = append(kinds, protocol.QuickFix)
|
|
}
|
|
fixes, err := suggestedAnalysisFixes(snapshot, pkg, e, kinds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if srcAnalyzer.Fix != "" {
|
|
cmd, err := command.NewApplyFixCommand(e.Message, command.ApplyFixArgs{
|
|
URI: protocol.URIFromSpanURI(spn.URI()),
|
|
Range: rng,
|
|
Fix: srcAnalyzer.Fix,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, kind := range kinds {
|
|
fixes = append(fixes, source.SuggestedFixFromCommand(cmd, kind))
|
|
}
|
|
}
|
|
related, err := relatedInformation(pkg, snapshot.FileSet(), e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
severity := srcAnalyzer.Severity
|
|
if severity == 0 {
|
|
severity = protocol.SeverityWarning
|
|
}
|
|
diag := &source.Diagnostic{
|
|
URI: spn.URI(),
|
|
Range: rng,
|
|
Severity: severity,
|
|
Source: source.AnalyzerErrorKind(e.Category),
|
|
Message: e.Message,
|
|
Related: related,
|
|
SuggestedFixes: fixes,
|
|
Analyzer: srcAnalyzer,
|
|
}
|
|
// If the fixes only delete code, assume that the diagnostic is reporting dead code.
|
|
if onlyDeletions(fixes) {
|
|
diag.Tags = []protocol.DiagnosticTag{protocol.Unnecessary}
|
|
}
|
|
return []*source.Diagnostic{diag}, nil
|
|
}
|
|
|
|
// onlyDeletions returns true if all of the suggested fixes are deletions.
|
|
func onlyDeletions(fixes []source.SuggestedFix) bool {
|
|
for _, fix := range fixes {
|
|
if fix.Command != nil {
|
|
return false
|
|
}
|
|
for _, edits := range fix.Edits {
|
|
for _, edit := range edits {
|
|
if edit.NewText != "" {
|
|
return false
|
|
}
|
|
if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return len(fixes) > 0
|
|
}
|
|
|
|
func typesCodeHref(snapshot *snapshot, code typesinternal.ErrorCode) string {
|
|
target := snapshot.View().Options().LinkTarget
|
|
return source.BuildLink(target, "golang.org/x/tools/internal/typesinternal", code.String())
|
|
}
|
|
|
|
func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic, kinds []protocol.CodeActionKind) ([]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.FileSet(), e.Pos, e.End).Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := spanToRange(pkg, spn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
edits[spn.URI()] = append(edits[spn.URI()], protocol.TextEdit{
|
|
Range: rng,
|
|
NewText: string(e.NewText),
|
|
})
|
|
}
|
|
for _, kind := range kinds {
|
|
fixes = append(fixes, source.SuggestedFix{
|
|
Title: fix.Message,
|
|
Edits: edits,
|
|
ActionKind: kind,
|
|
})
|
|
}
|
|
|
|
}
|
|
return fixes, nil
|
|
}
|
|
|
|
func relatedInformation(pkg *pkg, fset *token.FileSet, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) {
|
|
var out []source.RelatedInformation
|
|
for _, related := range diag.Related {
|
|
spn, err := span.NewRange(fset, related.Pos, related.End).Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := spanToRange(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 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)
|
|
}
|
|
|
|
// spanToRange converts a span.Span to a protocol.Range,
|
|
// assuming that the span belongs to the package whose diagnostics are being computed.
|
|
func spanToRange(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, wd string) span.Span {
|
|
input = strings.TrimSpace(input)
|
|
msgIndex := strings.Index(input, ": ")
|
|
if msgIndex < 0 {
|
|
return span.Parse(input)
|
|
}
|
|
return span.ParseInDir(input[:msgIndex], wd)
|
|
}
|
|
|
|
func parseGoListImportCycleError(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.FileSet(), imp.Pos(), imp.End()).Span()
|
|
if err != nil {
|
|
return msg, span.Span{}, false
|
|
}
|
|
return msg, spn, true
|
|
}
|
|
}
|
|
}
|
|
return msg, span.Span{}, false
|
|
}
|