golang.org/x/tools/gopls@v0.15.3/internal/cache/errors.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cache
     6  
     7  // This file defines routines to convert diagnostics from go list, go
     8  // get, go/packages, parsing, type checking, and analysis into
     9  // golang.Diagnostic form, and suggesting quick fixes.
    10  
    11  import (
    12  	"context"
    13  	"fmt"
    14  	"go/parser"
    15  	"go/scanner"
    16  	"go/token"
    17  	"log"
    18  	"path/filepath"
    19  	"regexp"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"golang.org/x/tools/go/packages"
    24  	"golang.org/x/tools/gopls/internal/cache/metadata"
    25  	"golang.org/x/tools/gopls/internal/file"
    26  	"golang.org/x/tools/gopls/internal/protocol"
    27  	"golang.org/x/tools/gopls/internal/protocol/command"
    28  	"golang.org/x/tools/gopls/internal/settings"
    29  	"golang.org/x/tools/gopls/internal/util/bug"
    30  	"golang.org/x/tools/internal/typesinternal"
    31  )
    32  
    33  // goPackagesErrorDiagnostics translates the given go/packages Error into a
    34  // diagnostic, using the provided metadata and filesource.
    35  //
    36  // The slice of diagnostics may be empty.
    37  func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, mp *metadata.Package, fs file.Source) ([]*Diagnostic, error) {
    38  	if diag, err := parseGoListImportCycleError(ctx, e, mp, fs); err != nil {
    39  		return nil, err
    40  	} else if diag != nil {
    41  		return []*Diagnostic{diag}, nil
    42  	}
    43  
    44  	// Parse error location and attempt to convert to protocol form.
    45  	loc, err := func() (protocol.Location, error) {
    46  		filename, line, col8 := parseGoListError(e, mp.LoadDir)
    47  		uri := protocol.URIFromPath(filename)
    48  
    49  		fh, err := fs.ReadFile(ctx, uri)
    50  		if err != nil {
    51  			return protocol.Location{}, err
    52  		}
    53  		content, err := fh.Content()
    54  		if err != nil {
    55  			return protocol.Location{}, err
    56  		}
    57  		mapper := protocol.NewMapper(uri, content)
    58  		posn, err := mapper.LineCol8Position(line, col8)
    59  		if err != nil {
    60  			return protocol.Location{}, err
    61  		}
    62  		return protocol.Location{
    63  			URI: uri,
    64  			Range: protocol.Range{
    65  				Start: posn,
    66  				End:   posn,
    67  			},
    68  		}, nil
    69  	}()
    70  
    71  	// TODO(rfindley): in some cases the go command outputs invalid spans, for
    72  	// example (from TestGoListErrors):
    73  	//
    74  	//   package a
    75  	//   import
    76  	//
    77  	// In this case, the go command will complain about a.go:2:8, which is after
    78  	// the trailing newline but still considered to be on the second line, most
    79  	// likely because *token.File lacks information about newline termination.
    80  	//
    81  	// We could do better here by handling that case.
    82  	if err != nil {
    83  		// Unable to parse a valid position.
    84  		// Apply the error to all files to be safe.
    85  		var diags []*Diagnostic
    86  		for _, uri := range mp.CompiledGoFiles {
    87  			diags = append(diags, &Diagnostic{
    88  				URI:      uri,
    89  				Severity: protocol.SeverityError,
    90  				Source:   ListError,
    91  				Message:  e.Msg,
    92  			})
    93  		}
    94  		return diags, nil
    95  	}
    96  	return []*Diagnostic{{
    97  		URI:      loc.URI,
    98  		Range:    loc.Range,
    99  		Severity: protocol.SeverityError,
   100  		Source:   ListError,
   101  		Message:  e.Msg,
   102  	}}, nil
   103  }
   104  
   105  func parseErrorDiagnostics(pkg *syntaxPackage, errList scanner.ErrorList) ([]*Diagnostic, error) {
   106  	// The first parser error is likely the root cause of the problem.
   107  	if errList.Len() <= 0 {
   108  		return nil, fmt.Errorf("no errors in %v", errList)
   109  	}
   110  	e := errList[0]
   111  	pgf, err := pkg.File(protocol.URIFromPath(e.Pos.Filename))
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	rng, err := pgf.Mapper.OffsetRange(e.Pos.Offset, e.Pos.Offset)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	return []*Diagnostic{{
   120  		URI:      pgf.URI,
   121  		Range:    rng,
   122  		Severity: protocol.SeverityError,
   123  		Source:   ParseError,
   124  		Message:  e.Msg,
   125  	}}, nil
   126  }
   127  
   128  var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
   129  var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`)
   130  
   131  func goGetQuickFixes(moduleMode bool, uri protocol.DocumentURI, pkg string) []SuggestedFix {
   132  	// Go get only supports module mode for now.
   133  	if !moduleMode {
   134  		return nil
   135  	}
   136  	title := fmt.Sprintf("go get package %v", pkg)
   137  	cmd, err := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{
   138  		URI:        uri,
   139  		AddRequire: true,
   140  		Pkg:        pkg,
   141  	})
   142  	if err != nil {
   143  		bug.Reportf("internal error building 'go get package' fix: %v", err)
   144  		return nil
   145  	}
   146  	return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}
   147  }
   148  
   149  func editGoDirectiveQuickFix(moduleMode bool, uri protocol.DocumentURI, version string) []SuggestedFix {
   150  	// Go mod edit only supports module mode.
   151  	if !moduleMode {
   152  		return nil
   153  	}
   154  	title := fmt.Sprintf("go mod edit -go=%s", version)
   155  	cmd, err := command.NewEditGoDirectiveCommand(title, command.EditGoDirectiveArgs{
   156  		URI:     uri,
   157  		Version: version,
   158  	})
   159  	if err != nil {
   160  		bug.Reportf("internal error constructing 'edit go directive' fix: %v", err)
   161  		return nil
   162  	}
   163  	return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}
   164  }
   165  
   166  // encodeDiagnostics gob-encodes the given diagnostics.
   167  func encodeDiagnostics(srcDiags []*Diagnostic) []byte {
   168  	var gobDiags []gobDiagnostic
   169  	for _, srcDiag := range srcDiags {
   170  		var gobFixes []gobSuggestedFix
   171  		for _, srcFix := range srcDiag.SuggestedFixes {
   172  			gobFix := gobSuggestedFix{
   173  				Message:    srcFix.Title,
   174  				ActionKind: srcFix.ActionKind,
   175  			}
   176  			for uri, srcEdits := range srcFix.Edits {
   177  				for _, srcEdit := range srcEdits {
   178  					gobFix.TextEdits = append(gobFix.TextEdits, gobTextEdit{
   179  						Location: protocol.Location{
   180  							URI:   uri,
   181  							Range: srcEdit.Range,
   182  						},
   183  						NewText: []byte(srcEdit.NewText),
   184  					})
   185  				}
   186  			}
   187  			if srcCmd := srcFix.Command; srcCmd != nil {
   188  				gobFix.Command = &gobCommand{
   189  					Title:     srcCmd.Title,
   190  					Command:   srcCmd.Command,
   191  					Arguments: srcCmd.Arguments,
   192  				}
   193  			}
   194  			gobFixes = append(gobFixes, gobFix)
   195  		}
   196  		var gobRelated []gobRelatedInformation
   197  		for _, srcRel := range srcDiag.Related {
   198  			gobRel := gobRelatedInformation(srcRel)
   199  			gobRelated = append(gobRelated, gobRel)
   200  		}
   201  		gobDiag := gobDiagnostic{
   202  			Location: protocol.Location{
   203  				URI:   srcDiag.URI,
   204  				Range: srcDiag.Range,
   205  			},
   206  			Severity:       srcDiag.Severity,
   207  			Code:           srcDiag.Code,
   208  			CodeHref:       srcDiag.CodeHref,
   209  			Source:         string(srcDiag.Source),
   210  			Message:        srcDiag.Message,
   211  			SuggestedFixes: gobFixes,
   212  			Related:        gobRelated,
   213  			Tags:           srcDiag.Tags,
   214  		}
   215  		gobDiags = append(gobDiags, gobDiag)
   216  	}
   217  	return diagnosticsCodec.Encode(gobDiags)
   218  }
   219  
   220  // decodeDiagnostics decodes the given gob-encoded diagnostics.
   221  func decodeDiagnostics(data []byte) []*Diagnostic {
   222  	var gobDiags []gobDiagnostic
   223  	diagnosticsCodec.Decode(data, &gobDiags)
   224  	var srcDiags []*Diagnostic
   225  	for _, gobDiag := range gobDiags {
   226  		var srcFixes []SuggestedFix
   227  		for _, gobFix := range gobDiag.SuggestedFixes {
   228  			srcFix := SuggestedFix{
   229  				Title:      gobFix.Message,
   230  				ActionKind: gobFix.ActionKind,
   231  			}
   232  			for _, gobEdit := range gobFix.TextEdits {
   233  				if srcFix.Edits == nil {
   234  					srcFix.Edits = make(map[protocol.DocumentURI][]protocol.TextEdit)
   235  				}
   236  				srcEdit := protocol.TextEdit{
   237  					Range:   gobEdit.Location.Range,
   238  					NewText: string(gobEdit.NewText),
   239  				}
   240  				uri := gobEdit.Location.URI
   241  				srcFix.Edits[uri] = append(srcFix.Edits[uri], srcEdit)
   242  			}
   243  			if gobCmd := gobFix.Command; gobCmd != nil {
   244  				srcFix.Command = &protocol.Command{
   245  					Title:     gobCmd.Title,
   246  					Command:   gobCmd.Command,
   247  					Arguments: gobCmd.Arguments,
   248  				}
   249  			}
   250  			srcFixes = append(srcFixes, srcFix)
   251  		}
   252  		var srcRelated []protocol.DiagnosticRelatedInformation
   253  		for _, gobRel := range gobDiag.Related {
   254  			srcRel := protocol.DiagnosticRelatedInformation(gobRel)
   255  			srcRelated = append(srcRelated, srcRel)
   256  		}
   257  		srcDiag := &Diagnostic{
   258  			URI:            gobDiag.Location.URI,
   259  			Range:          gobDiag.Location.Range,
   260  			Severity:       gobDiag.Severity,
   261  			Code:           gobDiag.Code,
   262  			CodeHref:       gobDiag.CodeHref,
   263  			Source:         DiagnosticSource(gobDiag.Source),
   264  			Message:        gobDiag.Message,
   265  			Tags:           gobDiag.Tags,
   266  			Related:        srcRelated,
   267  			SuggestedFixes: srcFixes,
   268  		}
   269  		srcDiags = append(srcDiags, srcDiag)
   270  	}
   271  	return srcDiags
   272  }
   273  
   274  // toSourceDiagnostic converts a gobDiagnostic to "source" form.
   275  func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) *Diagnostic {
   276  	var related []protocol.DiagnosticRelatedInformation
   277  	for _, gobRelated := range gobDiag.Related {
   278  		related = append(related, protocol.DiagnosticRelatedInformation(gobRelated))
   279  	}
   280  
   281  	severity := srcAnalyzer.Severity
   282  	if severity == 0 {
   283  		severity = protocol.SeverityWarning
   284  	}
   285  
   286  	diag := &Diagnostic{
   287  		URI:      gobDiag.Location.URI,
   288  		Range:    gobDiag.Location.Range,
   289  		Severity: severity,
   290  		Code:     gobDiag.Code,
   291  		CodeHref: gobDiag.CodeHref,
   292  		Source:   DiagnosticSource(gobDiag.Source),
   293  		Message:  gobDiag.Message,
   294  		Related:  related,
   295  		Tags:     srcAnalyzer.Tag,
   296  	}
   297  
   298  	// We cross the set of fixes (whether edit- or command-based)
   299  	// with the set of kinds, as a single fix may represent more
   300  	// than one kind of action (e.g. refactor, quickfix, fixall),
   301  	// each corresponding to a distinct client UI element
   302  	// or operation.
   303  	kinds := srcAnalyzer.ActionKinds
   304  	if len(kinds) == 0 {
   305  		kinds = []protocol.CodeActionKind{protocol.QuickFix}
   306  	}
   307  
   308  	var fixes []SuggestedFix
   309  	for _, fix := range gobDiag.SuggestedFixes {
   310  		if len(fix.TextEdits) > 0 {
   311  			// Accumulate edit-based fixes supplied by the diagnostic itself.
   312  			edits := make(map[protocol.DocumentURI][]protocol.TextEdit)
   313  			for _, e := range fix.TextEdits {
   314  				uri := e.Location.URI
   315  				edits[uri] = append(edits[uri], protocol.TextEdit{
   316  					Range:   e.Location.Range,
   317  					NewText: string(e.NewText),
   318  				})
   319  			}
   320  			for _, kind := range kinds {
   321  				fixes = append(fixes, SuggestedFix{
   322  					Title:      fix.Message,
   323  					Edits:      edits,
   324  					ActionKind: kind,
   325  				})
   326  			}
   327  
   328  		} else {
   329  			// Accumulate command-based fixes, whose edits
   330  			// are not provided by the analyzer but are computed on demand
   331  			// by logic "adjacent to" the analyzer.
   332  			//
   333  			// The analysis.Diagnostic.Category is used as the fix name.
   334  			cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{
   335  				Fix:   diag.Code,
   336  				URI:   gobDiag.Location.URI,
   337  				Range: gobDiag.Location.Range,
   338  			})
   339  			if err != nil {
   340  				// JSON marshalling of these argument values cannot fail.
   341  				log.Fatalf("internal error in NewApplyFixCommand: %v", err)
   342  			}
   343  			for _, kind := range kinds {
   344  				fixes = append(fixes, SuggestedFixFromCommand(cmd, kind))
   345  			}
   346  
   347  			// Ensure that the analyzer specifies a category for all its no-edit fixes.
   348  			// This is asserted by analysistest.RunWithSuggestedFixes, but there
   349  			// may be gaps in test coverage.
   350  			if diag.Code == "" || diag.Code == "default" {
   351  				bug.Reportf("missing Diagnostic.Code: %#v", *diag)
   352  			}
   353  		}
   354  	}
   355  	diag.SuggestedFixes = fixes
   356  
   357  	// If the fixes only delete code, assume that the diagnostic is reporting dead code.
   358  	if onlyDeletions(diag.SuggestedFixes) {
   359  		diag.Tags = append(diag.Tags, protocol.Unnecessary)
   360  	}
   361  	return diag
   362  }
   363  
   364  // onlyDeletions returns true if fixes is non-empty and all of the suggested
   365  // fixes are deletions.
   366  func onlyDeletions(fixes []SuggestedFix) bool {
   367  	for _, fix := range fixes {
   368  		if fix.Command != nil {
   369  			return false
   370  		}
   371  		for _, edits := range fix.Edits {
   372  			for _, edit := range edits {
   373  				if edit.NewText != "" {
   374  					return false
   375  				}
   376  				if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
   377  					return false
   378  				}
   379  			}
   380  		}
   381  	}
   382  	return len(fixes) > 0
   383  }
   384  
   385  func typesCodeHref(linkTarget string, code typesinternal.ErrorCode) string {
   386  	return BuildLink(linkTarget, "golang.org/x/tools/internal/typesinternal", code.String())
   387  }
   388  
   389  // BuildLink constructs a URL with the given target, path, and anchor.
   390  func BuildLink(target, path, anchor string) string {
   391  	link := fmt.Sprintf("https://%s/%s", target, path)
   392  	if anchor == "" {
   393  		return link
   394  	}
   395  	return link + "#" + anchor
   396  }
   397  
   398  func parseGoListError(e packages.Error, dir string) (filename string, line, col8 int) {
   399  	input := e.Pos
   400  	if input == "" {
   401  		// No position. Attempt to parse one out of a
   402  		// go list error of the form "file:line:col:
   403  		// message" by stripping off the message.
   404  		input = strings.TrimSpace(e.Msg)
   405  		if i := strings.Index(input, ": "); i >= 0 {
   406  			input = input[:i]
   407  		}
   408  	}
   409  
   410  	filename, line, col8 = splitFileLineCol(input)
   411  	if !filepath.IsAbs(filename) {
   412  		filename = filepath.Join(dir, filename)
   413  	}
   414  	return filename, line, col8
   415  }
   416  
   417  // splitFileLineCol splits s into "filename:line:col",
   418  // where line and col consist of decimal digits.
   419  func splitFileLineCol(s string) (file string, line, col8 int) {
   420  	// Beware that the filename may contain colon on Windows.
   421  
   422  	// stripColonDigits removes a ":%d" suffix, if any.
   423  	stripColonDigits := func(s string) (rest string, num int) {
   424  		if i := strings.LastIndex(s, ":"); i >= 0 {
   425  			if v, err := strconv.ParseInt(s[i+1:], 10, 32); err == nil {
   426  				return s[:i], int(v)
   427  			}
   428  		}
   429  		return s, -1
   430  	}
   431  
   432  	// strip col ":%d"
   433  	s, n1 := stripColonDigits(s)
   434  	if n1 < 0 {
   435  		return s, 0, 0 // "filename"
   436  	}
   437  
   438  	// strip line ":%d"
   439  	s, n2 := stripColonDigits(s)
   440  	if n2 < 0 {
   441  		return s, n1, 0 // "filename:line"
   442  	}
   443  
   444  	return s, n2, n1 // "filename:line:col"
   445  }
   446  
   447  // parseGoListImportCycleError attempts to parse the given go/packages error as
   448  // an import cycle, returning a diagnostic if successful.
   449  //
   450  // If the error is not detected as an import cycle error, it returns nil, nil.
   451  func parseGoListImportCycleError(ctx context.Context, e packages.Error, mp *metadata.Package, fs file.Source) (*Diagnostic, error) {
   452  	re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`)
   453  	matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg))
   454  	if len(matches) < 3 {
   455  		return nil, nil
   456  	}
   457  	msg := matches[1]
   458  	importList := strings.Split(matches[2], " ")
   459  	// Since the error is relative to the current package. The import that is causing
   460  	// the import cycle error is the second one in the list.
   461  	if len(importList) < 2 {
   462  		return nil, nil
   463  	}
   464  	// Imports have quotation marks around them.
   465  	circImp := strconv.Quote(importList[1])
   466  	for _, uri := range mp.CompiledGoFiles {
   467  		pgf, err := parseGoURI(ctx, fs, uri, ParseHeader)
   468  		if err != nil {
   469  			return nil, err
   470  		}
   471  		// Search file imports for the import that is causing the import cycle.
   472  		for _, imp := range pgf.File.Imports {
   473  			if imp.Path.Value == circImp {
   474  				rng, err := pgf.NodeMappedRange(imp)
   475  				if err != nil {
   476  					return nil, nil
   477  				}
   478  
   479  				return &Diagnostic{
   480  					URI:      pgf.URI,
   481  					Range:    rng.Range(),
   482  					Severity: protocol.SeverityError,
   483  					Source:   ListError,
   484  					Message:  msg,
   485  				}, nil
   486  			}
   487  		}
   488  	}
   489  	return nil, nil
   490  }
   491  
   492  // parseGoURI is a helper to parse the Go file at the given URI from the file
   493  // source fs. The resulting syntax and token.File belong to an ephemeral,
   494  // encapsulated FileSet, so this file stands only on its own: it's not suitable
   495  // to use in a list of file of a package, for example.
   496  //
   497  // It returns an error if the file could not be read.
   498  //
   499  // TODO(rfindley): eliminate this helper.
   500  func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*ParsedGoFile, error) {
   501  	fh, err := fs.ReadFile(ctx, uri)
   502  	if err != nil {
   503  		return nil, err
   504  	}
   505  	return parseGoImpl(ctx, token.NewFileSet(), fh, mode, false)
   506  }
   507  
   508  // parseModURI is a helper to parse the Mod file at the given URI from the file
   509  // source fs.
   510  //
   511  // It returns an error if the file could not be read.
   512  func parseModURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI) (*ParsedModule, error) {
   513  	fh, err := fs.ReadFile(ctx, uri)
   514  	if err != nil {
   515  		return nil, err
   516  	}
   517  	return parseModImpl(ctx, fh)
   518  }