aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/cache/errors.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/cache/errors.go')
-rw-r--r--gopls/internal/lsp/cache/errors.go528
1 files changed, 528 insertions, 0 deletions
diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go
new file mode 100644
index 000000000..07783f4b3
--- /dev/null
+++ b/gopls/internal/lsp/cache/errors.go
@@ -0,0 +1,528 @@
+// 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
+
+// This file defines routines to convert diagnostics from go list, go
+// get, go/packages, parsing, type checking, and analysis into
+// source.Diagnostic form, and suggesting quick fixes.
+
+import (
+ "context"
+ "fmt"
+ "go/scanner"
+ "go/token"
+ "go/types"
+ "log"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/bug"
+ "golang.org/x/tools/internal/typesinternal"
+)
+
+// goPackagesErrorDiagnostics translates the given go/packages Error into a
+// diagnostic, using the provided metadata and filesource.
+//
+// The slice of diagnostics may be empty.
+func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) ([]*source.Diagnostic, error) {
+ if diag, err := parseGoListImportCycleError(ctx, e, m, fs); err != nil {
+ return nil, err
+ } else if diag != nil {
+ return []*source.Diagnostic{diag}, nil
+ }
+
+ var spn span.Span
+ if e.Pos == "" {
+ spn = parseGoListError(e.Msg, m.LoadDir)
+ // We may not have been able to parse a valid span. Apply the errors to all files.
+ if _, err := spanToRange(ctx, fs, spn); err != nil {
+ var diags []*source.Diagnostic
+ for _, uri := range m.CompiledGoFiles {
+ diags = append(diags, &source.Diagnostic{
+ URI: uri,
+ Severity: protocol.SeverityError,
+ Source: source.ListError,
+ Message: e.Msg,
+ })
+ }
+ return diags, nil
+ }
+ } else {
+ spn = span.ParseInDir(e.Pos, m.LoadDir)
+ }
+
+ // TODO(rfindley): in some cases the go command outputs invalid spans, for
+ // example (from TestGoListErrors):
+ //
+ // package a
+ // import
+ //
+ // In this case, the go command will complain about a.go:2:8, which is after
+ // the trailing newline but still considered to be on the second line, most
+ // likely because *token.File lacks information about newline termination.
+ //
+ // We could do better here by handling that case.
+ rng, err := spanToRange(ctx, fs, 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(pkg *syntaxPackage, errList scanner.ErrorList) ([]*source.Diagnostic, error) {
+ // The first parser error is likely the root cause of the problem.
+ if errList.Len() <= 0 {
+ return nil, fmt.Errorf("no errors in %v", errList)
+ }
+ e := errList[0]
+ pgf, err := pkg.File(span.URIFromPath(e.Pos.Filename))
+ if err != nil {
+ return nil, err
+ }
+ rng, err := pgf.Mapper.OffsetRange(e.Pos.Offset, e.Pos.Offset)
+ if err != nil {
+ return nil, err
+ }
+ return []*source.Diagnostic{{
+ URI: pgf.URI,
+ Range: rng,
+ Severity: protocol.SeverityError,
+ Source: source.ParseError,
+ Message: e.Msg,
+ }}, nil
+}
+
+var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
+var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`)
+
+func typeErrorDiagnostics(moduleMode bool, linkTarget string, pkg *syntaxPackage, e extendedError) ([]*source.Diagnostic, error) {
+ code, loc, err := typeErrorData(pkg, e.primary)
+ if err != nil {
+ return nil, err
+ }
+ diag := &source.Diagnostic{
+ URI: loc.URI.SpanURI(),
+ Range: loc.Range,
+ Severity: protocol.SeverityError,
+ Source: source.TypeError,
+ Message: e.primary.Msg,
+ }
+ if code != 0 {
+ diag.Code = code.String()
+ diag.CodeHref = typesCodeHref(linkTarget, code)
+ }
+ switch code {
+ case typesinternal.UnusedVar, typesinternal.UnusedImport:
+ diag.Tags = append(diag.Tags, protocol.Unnecessary)
+ }
+
+ for _, secondary := range e.secondaries {
+ _, secondaryLoc, err := typeErrorData(pkg, secondary)
+ if err != nil {
+ return nil, err
+ }
+ diag.Related = append(diag.Related, protocol.DiagnosticRelatedInformation{
+ Location: secondaryLoc,
+ Message: secondary.Msg,
+ })
+ }
+
+ if match := importErrorRe.FindStringSubmatch(e.primary.Msg); match != nil {
+ diag.SuggestedFixes, err = goGetQuickFixes(moduleMode, loc.URI.SpanURI(), match[1])
+ if err != nil {
+ return nil, err
+ }
+ }
+ if match := unsupportedFeatureRe.FindStringSubmatch(e.primary.Msg); match != nil {
+ diag.SuggestedFixes, err = editGoDirectiveQuickFix(moduleMode, loc.URI.SpanURI(), match[1])
+ if err != nil {
+ return nil, err
+ }
+ }
+ return []*source.Diagnostic{diag}, nil
+}
+
+func goGetQuickFixes(moduleMode bool, uri span.URI, pkg string) ([]source.SuggestedFix, error) {
+ // Go get only supports module mode for now.
+ if !moduleMode {
+ 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 editGoDirectiveQuickFix(moduleMode bool, uri span.URI, version string) ([]source.SuggestedFix, error) {
+ // Go mod edit only supports module mode.
+ if !moduleMode {
+ return nil, nil
+ }
+ title := fmt.Sprintf("go mod edit -go=%s", version)
+ cmd, err := command.NewEditGoDirectiveCommand(title, command.EditGoDirectiveArgs{
+ URI: protocol.URIFromSpanURI(uri),
+ Version: version,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, nil
+}
+
+// encodeDiagnostics gob-encodes the given diagnostics.
+func encodeDiagnostics(srcDiags []*source.Diagnostic) []byte {
+ var gobDiags []gobDiagnostic
+ for _, srcDiag := range srcDiags {
+ var gobFixes []gobSuggestedFix
+ for _, srcFix := range srcDiag.SuggestedFixes {
+ gobFix := gobSuggestedFix{
+ Message: srcFix.Title,
+ ActionKind: srcFix.ActionKind,
+ }
+ for uri, srcEdits := range srcFix.Edits {
+ for _, srcEdit := range srcEdits {
+ gobFix.TextEdits = append(gobFix.TextEdits, gobTextEdit{
+ Location: protocol.Location{
+ URI: protocol.URIFromSpanURI(uri),
+ Range: srcEdit.Range,
+ },
+ NewText: []byte(srcEdit.NewText),
+ })
+ }
+ }
+ if srcCmd := srcFix.Command; srcCmd != nil {
+ gobFix.Command = &gobCommand{
+ Title: srcCmd.Title,
+ Command: srcCmd.Command,
+ Arguments: srcCmd.Arguments,
+ }
+ }
+ gobFixes = append(gobFixes, gobFix)
+ }
+ var gobRelated []gobRelatedInformation
+ for _, srcRel := range srcDiag.Related {
+ gobRel := gobRelatedInformation(srcRel)
+ gobRelated = append(gobRelated, gobRel)
+ }
+ gobDiag := gobDiagnostic{
+ Location: protocol.Location{
+ URI: protocol.URIFromSpanURI(srcDiag.URI),
+ Range: srcDiag.Range,
+ },
+ Severity: srcDiag.Severity,
+ Code: srcDiag.Code,
+ CodeHref: srcDiag.CodeHref,
+ Source: string(srcDiag.Source),
+ Message: srcDiag.Message,
+ SuggestedFixes: gobFixes,
+ Related: gobRelated,
+ Tags: srcDiag.Tags,
+ }
+ gobDiags = append(gobDiags, gobDiag)
+ }
+ return mustEncode(gobDiags)
+}
+
+// decodeDiagnostics decodes the given gob-encoded diagnostics.
+func decodeDiagnostics(data []byte) []*source.Diagnostic {
+ var gobDiags []gobDiagnostic
+ mustDecode(data, &gobDiags)
+ var srcDiags []*source.Diagnostic
+ for _, gobDiag := range gobDiags {
+ var srcFixes []source.SuggestedFix
+ for _, gobFix := range gobDiag.SuggestedFixes {
+ srcFix := source.SuggestedFix{
+ Title: gobFix.Message,
+ ActionKind: gobFix.ActionKind,
+ }
+ for _, gobEdit := range gobFix.TextEdits {
+ if srcFix.Edits == nil {
+ srcFix.Edits = make(map[span.URI][]protocol.TextEdit)
+ }
+ srcEdit := protocol.TextEdit{
+ Range: gobEdit.Location.Range,
+ NewText: string(gobEdit.NewText),
+ }
+ uri := gobEdit.Location.URI.SpanURI()
+ srcFix.Edits[uri] = append(srcFix.Edits[uri], srcEdit)
+ }
+ if gobCmd := gobFix.Command; gobCmd != nil {
+ gobFix.Command = &gobCommand{
+ Title: gobCmd.Title,
+ Command: gobCmd.Command,
+ Arguments: gobCmd.Arguments,
+ }
+ }
+ srcFixes = append(srcFixes, srcFix)
+ }
+ var srcRelated []protocol.DiagnosticRelatedInformation
+ for _, gobRel := range gobDiag.Related {
+ srcRel := protocol.DiagnosticRelatedInformation(gobRel)
+ srcRelated = append(srcRelated, srcRel)
+ }
+ srcDiag := &source.Diagnostic{
+ URI: gobDiag.Location.URI.SpanURI(),
+ Range: gobDiag.Location.Range,
+ Severity: gobDiag.Severity,
+ Source: source.AnalyzerErrorKind(gobDiag.Source),
+ Message: gobDiag.Message,
+ Tags: gobDiag.Tags,
+ Related: srcRelated,
+ SuggestedFixes: srcFixes,
+ }
+ srcDiags = append(srcDiags, srcDiag)
+ }
+ return srcDiags
+}
+
+// toSourceDiagnostic converts a gobDiagnostic to "source" form.
+func toSourceDiagnostic(srcAnalyzer *source.Analyzer, gobDiag *gobDiagnostic) *source.Diagnostic {
+ var related []protocol.DiagnosticRelatedInformation
+ for _, gobRelated := range gobDiag.Related {
+ related = append(related, protocol.DiagnosticRelatedInformation(gobRelated))
+ }
+
+ kinds := srcAnalyzer.ActionKind
+ if len(srcAnalyzer.ActionKind) == 0 {
+ kinds = append(kinds, protocol.QuickFix)
+ }
+ fixes := suggestedAnalysisFixes(gobDiag, kinds)
+ if srcAnalyzer.Fix != "" {
+ cmd, err := command.NewApplyFixCommand(gobDiag.Message, command.ApplyFixArgs{
+ URI: gobDiag.Location.URI,
+ Range: gobDiag.Location.Range,
+ Fix: srcAnalyzer.Fix,
+ })
+ if err != nil {
+ // JSON marshalling of these argument values cannot fail.
+ log.Fatalf("internal error in NewApplyFixCommand: %v", err)
+ }
+ for _, kind := range kinds {
+ fixes = append(fixes, source.SuggestedFixFromCommand(cmd, kind))
+ }
+ }
+
+ severity := srcAnalyzer.Severity
+ if severity == 0 {
+ severity = protocol.SeverityWarning
+ }
+
+ diag := &source.Diagnostic{
+ URI: gobDiag.Location.URI.SpanURI(),
+ Range: gobDiag.Location.Range,
+ Severity: severity,
+ Source: source.AnalyzerErrorKind(gobDiag.Source),
+ Message: gobDiag.Message,
+ Related: related,
+ SuggestedFixes: fixes,
+ }
+ // If the fixes only delete code, assume that the diagnostic is reporting dead code.
+ if onlyDeletions(fixes) {
+ diag.Tags = []protocol.DiagnosticTag{protocol.Unnecessary}
+ }
+ return diag
+}
+
+// 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(linkTarget string, code typesinternal.ErrorCode) string {
+ return source.BuildLink(linkTarget, "golang.org/x/tools/internal/typesinternal", code.String())
+}
+
+func suggestedAnalysisFixes(diag *gobDiagnostic, kinds []protocol.CodeActionKind) []source.SuggestedFix {
+ var fixes []source.SuggestedFix
+ for _, fix := range diag.SuggestedFixes {
+ edits := make(map[span.URI][]protocol.TextEdit)
+ for _, e := range fix.TextEdits {
+ uri := span.URI(e.Location.URI)
+ edits[uri] = append(edits[uri], protocol.TextEdit{
+ Range: e.Location.Range,
+ NewText: string(e.NewText),
+ })
+ }
+ for _, kind := range kinds {
+ fixes = append(fixes, source.SuggestedFix{
+ Title: fix.Message,
+ Edits: edits,
+ ActionKind: kind,
+ })
+ }
+
+ }
+ return fixes
+}
+
+func typeErrorData(pkg *syntaxPackage, terr types.Error) (typesinternal.ErrorCode, protocol.Location, error) {
+ ecode, start, end, ok := typesinternal.ReadGo116ErrorData(terr)
+ if !ok {
+ start, end = terr.Pos, terr.Pos
+ ecode = 0
+ }
+ // go/types may return invalid positions in some cases, such as
+ // in errors on tokens missing from the syntax tree.
+ if !start.IsValid() {
+ return 0, protocol.Location{}, fmt.Errorf("type error (%q, code %d, go116=%t) without position", terr.Msg, ecode, ok)
+ }
+ // go/types errors retain their FileSet.
+ // Sanity-check that we're using the right one.
+ fset := pkg.fset
+ if fset != terr.Fset {
+ return 0, protocol.Location{}, bug.Errorf("wrong FileSet for type error")
+ }
+ posn := safetoken.StartPosition(fset, start)
+ if !posn.IsValid() {
+ return 0, protocol.Location{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr)
+ }
+ pgf, err := pkg.File(span.URIFromPath(posn.Filename))
+ if err != nil {
+ return 0, protocol.Location{}, err
+ }
+ if !end.IsValid() || end == start {
+ end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start)
+ }
+ loc, err := pgf.Mapper.PosLocation(pgf.Tok, start, end)
+ return ecode, loc, err
+}
+
+// spanToRange converts a span.Span to a protocol.Range, by mapping content
+// contained in the provided FileSource.
+func spanToRange(ctx context.Context, fs source.FileSource, spn span.Span) (protocol.Range, error) {
+ uri := spn.URI()
+ fh, err := fs.GetFile(ctx, uri)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ content, err := fh.Read()
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ mapper := protocol.NewMapper(uri, content)
+ return mapper.SpanRange(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)
+}
+
+// parseGoListImportCycleError attempts to parse the given go/packages error as
+// an import cycle, returning a diagnostic if successful.
+//
+// If the error is not detected as an import cycle error, it returns nil, nil.
+func parseGoListImportCycleError(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) (*source.Diagnostic, error) {
+ re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`)
+ matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg))
+ if len(matches) < 3 {
+ return nil, nil
+ }
+ 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 nil, nil
+ }
+ // Imports have quotation marks around them.
+ circImp := strconv.Quote(importList[1])
+ for _, uri := range m.CompiledGoFiles {
+ pgf, err := parseGoURI(ctx, fs, uri, source.ParseHeader)
+ if err != nil {
+ return nil, err
+ }
+ // Search file imports for the import that is causing the import cycle.
+ for _, imp := range pgf.File.Imports {
+ if imp.Path.Value == circImp {
+ rng, err := pgf.NodeMappedRange(imp)
+ if err != nil {
+ return nil, nil
+ }
+
+ return &source.Diagnostic{
+ URI: pgf.URI,
+ Range: rng.Range(),
+ Severity: protocol.SeverityError,
+ Source: source.ListError,
+ Message: msg,
+ }, nil
+ }
+ }
+ }
+ return nil, nil
+}
+
+// parseGoURI is a helper to parse the Go file at the given URI from the file
+// source fs. The resulting syntax and token.File belong to an ephemeral,
+// encapsulated FileSet, so this file stands only on its own: it's not suitable
+// to use in a list of file of a package, for example.
+//
+// It returns an error if the file could not be read.
+func parseGoURI(ctx context.Context, fs source.FileSource, uri span.URI, mode source.ParseMode) (*source.ParsedGoFile, error) {
+ fh, err := fs.GetFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ return parseGoImpl(ctx, token.NewFileSet(), fh, source.ParseHeader)
+}
+
+// parseModURI is a helper to parse the Mod file at the given URI from the file
+// source fs.
+//
+// It returns an error if the file could not be read.
+func parseModURI(ctx context.Context, fs source.FileSource, uri span.URI) (*source.ParsedModule, error) {
+ fh, err := fs.GetFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ return parseModImpl(ctx, fh)
+}