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