golang.org/x/tools/gopls@v0.15.3/internal/cache/mod.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  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"golang.org/x/mod/modfile"
    16  	"golang.org/x/mod/module"
    17  	"golang.org/x/tools/gopls/internal/file"
    18  	"golang.org/x/tools/gopls/internal/protocol"
    19  	"golang.org/x/tools/gopls/internal/protocol/command"
    20  	"golang.org/x/tools/internal/event"
    21  	"golang.org/x/tools/internal/event/tag"
    22  	"golang.org/x/tools/internal/gocommand"
    23  	"golang.org/x/tools/internal/memoize"
    24  )
    25  
    26  // A ParsedModule contains the results of parsing a go.mod file.
    27  type ParsedModule struct {
    28  	URI         protocol.DocumentURI
    29  	File        *modfile.File
    30  	Mapper      *protocol.Mapper
    31  	ParseErrors []*Diagnostic
    32  }
    33  
    34  // ParseMod parses a go.mod file, using a cache. It may return partial results and an error.
    35  func (s *Snapshot) ParseMod(ctx context.Context, fh file.Handle) (*ParsedModule, error) {
    36  	uri := fh.URI()
    37  
    38  	s.mu.Lock()
    39  	entry, hit := s.parseModHandles.Get(uri)
    40  	s.mu.Unlock()
    41  
    42  	type parseModKey file.Identity
    43  	type parseModResult struct {
    44  		parsed *ParsedModule
    45  		err    error
    46  	}
    47  
    48  	// cache miss?
    49  	if !hit {
    50  		promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} {
    51  			parsed, err := parseModImpl(ctx, fh)
    52  			return parseModResult{parsed, err}
    53  		})
    54  
    55  		entry = promise
    56  		s.mu.Lock()
    57  		s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() })
    58  		s.mu.Unlock()
    59  	}
    60  
    61  	// Await result.
    62  	v, err := s.awaitPromise(ctx, entry)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	res := v.(parseModResult)
    67  	return res.parsed, res.err
    68  }
    69  
    70  // parseModImpl parses the go.mod file whose name and contents are in fh.
    71  // It may return partial results and an error.
    72  func parseModImpl(ctx context.Context, fh file.Handle) (*ParsedModule, error) {
    73  	_, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI()))
    74  	defer done()
    75  
    76  	contents, err := fh.Content()
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	m := protocol.NewMapper(fh.URI(), contents)
    81  	file, parseErr := modfile.Parse(fh.URI().Path(), contents, nil)
    82  	// Attempt to convert the error to a standardized parse error.
    83  	var parseErrors []*Diagnostic
    84  	if parseErr != nil {
    85  		mfErrList, ok := parseErr.(modfile.ErrorList)
    86  		if !ok {
    87  			return nil, fmt.Errorf("unexpected parse error type %v", parseErr)
    88  		}
    89  		for _, mfErr := range mfErrList {
    90  			rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte)
    91  			if err != nil {
    92  				return nil, err
    93  			}
    94  			parseErrors = append(parseErrors, &Diagnostic{
    95  				URI:      fh.URI(),
    96  				Range:    rng,
    97  				Severity: protocol.SeverityError,
    98  				Source:   ParseError,
    99  				Message:  mfErr.Err.Error(),
   100  			})
   101  		}
   102  	}
   103  	return &ParsedModule{
   104  		URI:         fh.URI(),
   105  		Mapper:      m,
   106  		File:        file,
   107  		ParseErrors: parseErrors,
   108  	}, parseErr
   109  }
   110  
   111  // A ParsedWorkFile contains the results of parsing a go.work file.
   112  type ParsedWorkFile struct {
   113  	URI         protocol.DocumentURI
   114  	File        *modfile.WorkFile
   115  	Mapper      *protocol.Mapper
   116  	ParseErrors []*Diagnostic
   117  }
   118  
   119  // ParseWork parses a go.work file, using a cache. It may return partial results and an error.
   120  // TODO(adonovan): move to new work.go file.
   121  func (s *Snapshot) ParseWork(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) {
   122  	uri := fh.URI()
   123  
   124  	s.mu.Lock()
   125  	entry, hit := s.parseWorkHandles.Get(uri)
   126  	s.mu.Unlock()
   127  
   128  	type parseWorkKey file.Identity
   129  	type parseWorkResult struct {
   130  		parsed *ParsedWorkFile
   131  		err    error
   132  	}
   133  
   134  	// cache miss?
   135  	if !hit {
   136  		handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} {
   137  			parsed, err := parseWorkImpl(ctx, fh)
   138  			return parseWorkResult{parsed, err}
   139  		})
   140  
   141  		entry = handle
   142  		s.mu.Lock()
   143  		s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() })
   144  		s.mu.Unlock()
   145  	}
   146  
   147  	// Await result.
   148  	v, err := s.awaitPromise(ctx, entry)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	res := v.(parseWorkResult)
   153  	return res.parsed, res.err
   154  }
   155  
   156  // parseWorkImpl parses a go.work file. It may return partial results and an error.
   157  func parseWorkImpl(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) {
   158  	_, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI()))
   159  	defer done()
   160  
   161  	content, err := fh.Content()
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	m := protocol.NewMapper(fh.URI(), content)
   166  	file, parseErr := modfile.ParseWork(fh.URI().Path(), content, nil)
   167  	// Attempt to convert the error to a standardized parse error.
   168  	var parseErrors []*Diagnostic
   169  	if parseErr != nil {
   170  		mfErrList, ok := parseErr.(modfile.ErrorList)
   171  		if !ok {
   172  			return nil, fmt.Errorf("unexpected parse error type %v", parseErr)
   173  		}
   174  		for _, mfErr := range mfErrList {
   175  			rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte)
   176  			if err != nil {
   177  				return nil, err
   178  			}
   179  			parseErrors = append(parseErrors, &Diagnostic{
   180  				URI:      fh.URI(),
   181  				Range:    rng,
   182  				Severity: protocol.SeverityError,
   183  				Source:   ParseError,
   184  				Message:  mfErr.Err.Error(),
   185  			})
   186  		}
   187  	}
   188  	return &ParsedWorkFile{
   189  		URI:         fh.URI(),
   190  		Mapper:      m,
   191  		File:        file,
   192  		ParseErrors: parseErrors,
   193  	}, parseErr
   194  }
   195  
   196  // goSum reads the go.sum file for the go.mod file at modURI, if it exists. If
   197  // it doesn't exist, it returns nil.
   198  func (s *Snapshot) goSum(ctx context.Context, modURI protocol.DocumentURI) []byte {
   199  	// Get the go.sum file, either from the snapshot or directly from the
   200  	// cache. Avoid (*snapshot).ReadFile here, as we don't want to add
   201  	// nonexistent file handles to the snapshot if the file does not exist.
   202  	//
   203  	// TODO(rfindley): but that's not right. Changes to sum files should
   204  	// invalidate content, even if it's nonexistent content.
   205  	sumURI := protocol.URIFromPath(sumFilename(modURI))
   206  	sumFH := s.FindFile(sumURI)
   207  	if sumFH == nil {
   208  		var err error
   209  		sumFH, err = s.view.fs.ReadFile(ctx, sumURI)
   210  		if err != nil {
   211  			return nil
   212  		}
   213  	}
   214  	content, err := sumFH.Content()
   215  	if err != nil {
   216  		return nil
   217  	}
   218  	return content
   219  }
   220  
   221  func sumFilename(modURI protocol.DocumentURI) string {
   222  	return strings.TrimSuffix(modURI.Path(), ".mod") + ".sum"
   223  }
   224  
   225  // ModWhy returns the "go mod why" result for each module named in a
   226  // require statement in the go.mod file.
   227  // TODO(adonovan): move to new mod_why.go file.
   228  func (s *Snapshot) ModWhy(ctx context.Context, fh file.Handle) (map[string]string, error) {
   229  	uri := fh.URI()
   230  
   231  	if s.FileKind(fh) != file.Mod {
   232  		return nil, fmt.Errorf("%s is not a go.mod file", uri)
   233  	}
   234  
   235  	s.mu.Lock()
   236  	entry, hit := s.modWhyHandles.Get(uri)
   237  	s.mu.Unlock()
   238  
   239  	type modWhyResult struct {
   240  		why map[string]string
   241  		err error
   242  	}
   243  
   244  	// cache miss?
   245  	if !hit {
   246  		handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} {
   247  			why, err := modWhyImpl(ctx, arg.(*Snapshot), fh)
   248  			return modWhyResult{why, err}
   249  		})
   250  
   251  		entry = handle
   252  		s.mu.Lock()
   253  		s.modWhyHandles.Set(uri, entry, nil)
   254  		s.mu.Unlock()
   255  	}
   256  
   257  	// Await result.
   258  	v, err := s.awaitPromise(ctx, entry)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	res := v.(modWhyResult)
   263  	return res.why, res.err
   264  }
   265  
   266  // modWhyImpl returns the result of "go mod why -m" on the specified go.mod file.
   267  func modWhyImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) (map[string]string, error) {
   268  	ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI()))
   269  	defer done()
   270  
   271  	pm, err := snapshot.ParseMod(ctx, fh)
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	// No requires to explain.
   276  	if len(pm.File.Require) == 0 {
   277  		return nil, nil // empty result
   278  	}
   279  	// Run `go mod why` on all the dependencies.
   280  	inv := &gocommand.Invocation{
   281  		Verb:       "mod",
   282  		Args:       []string{"why", "-m"},
   283  		WorkingDir: filepath.Dir(fh.URI().Path()),
   284  	}
   285  	for _, req := range pm.File.Require {
   286  		inv.Args = append(inv.Args, req.Mod.Path)
   287  	}
   288  	stdout, err := snapshot.RunGoCommandDirect(ctx, Normal, inv)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  	whyList := strings.Split(stdout.String(), "\n\n")
   293  	if len(whyList) != len(pm.File.Require) {
   294  		return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require))
   295  	}
   296  	why := make(map[string]string, len(pm.File.Require))
   297  	for i, req := range pm.File.Require {
   298  		why[req.Mod.Path] = whyList[i]
   299  	}
   300  	return why, nil
   301  }
   302  
   303  // extractGoCommandErrors tries to parse errors that come from the go command
   304  // and shape them into go.mod diagnostics.
   305  // TODO: rename this to 'load errors'
   306  func (s *Snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) []*Diagnostic {
   307  	if goCmdError == nil {
   308  		return nil
   309  	}
   310  
   311  	type locatedErr struct {
   312  		loc protocol.Location
   313  		msg string
   314  	}
   315  	diagLocations := map[*ParsedModule]locatedErr{}
   316  	backupDiagLocations := map[*ParsedModule]locatedErr{}
   317  
   318  	// If moduleErrs is non-nil, go command errors are scoped to specific
   319  	// modules.
   320  	var moduleErrs *moduleErrorMap
   321  	_ = errors.As(goCmdError, &moduleErrs)
   322  
   323  	// Match the error against all the mod files in the workspace.
   324  	for _, uri := range s.View().ModFiles() {
   325  		fh, err := s.ReadFile(ctx, uri)
   326  		if err != nil {
   327  			event.Error(ctx, "getting modfile for Go command error", err)
   328  			continue
   329  		}
   330  		pm, err := s.ParseMod(ctx, fh)
   331  		if err != nil {
   332  			// Parsing errors are reported elsewhere
   333  			return nil
   334  		}
   335  		var msgs []string // error messages to consider
   336  		if moduleErrs != nil {
   337  			if pm.File.Module != nil {
   338  				for _, mes := range moduleErrs.errs[pm.File.Module.Mod.Path] {
   339  					msgs = append(msgs, mes.Error())
   340  				}
   341  			}
   342  		} else {
   343  			msgs = append(msgs, goCmdError.Error())
   344  		}
   345  		for _, msg := range msgs {
   346  			if strings.Contains(goCmdError.Error(), "errors parsing go.mod") {
   347  				// The go command emits parse errors for completely invalid go.mod files.
   348  				// Those are reported by our own diagnostics and can be ignored here.
   349  				// As of writing, we are not aware of any other errors that include
   350  				// file/position information, so don't even try to find it.
   351  				continue
   352  			}
   353  			loc, found, err := s.matchErrorToModule(pm, msg)
   354  			if err != nil {
   355  				event.Error(ctx, "matching error to module", err)
   356  				continue
   357  			}
   358  			le := locatedErr{
   359  				loc: loc,
   360  				msg: msg,
   361  			}
   362  			if found {
   363  				diagLocations[pm] = le
   364  			} else {
   365  				backupDiagLocations[pm] = le
   366  			}
   367  		}
   368  	}
   369  
   370  	// If we didn't find any good matches, assign diagnostics to all go.mod files.
   371  	if len(diagLocations) == 0 {
   372  		diagLocations = backupDiagLocations
   373  	}
   374  
   375  	var srcErrs []*Diagnostic
   376  	for pm, le := range diagLocations {
   377  		diag, err := s.goCommandDiagnostic(pm, le.loc, le.msg)
   378  		if err != nil {
   379  			event.Error(ctx, "building go command diagnostic", err)
   380  			continue
   381  		}
   382  		srcErrs = append(srcErrs, diag)
   383  	}
   384  	return srcErrs
   385  }
   386  
   387  var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`)
   388  
   389  // matchErrorToModule matches a go command error message to a go.mod file.
   390  // Some examples:
   391  //
   392  //	example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory
   393  //	go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72
   394  //	go: example.com@v1.2.3 requires\n\trandom.org@v1.2.3: parsing go.mod:\n\tmodule declares its path as: bob.org\n\tbut was required as: random.org
   395  //
   396  // It returns the location of a reference to the one of the modules and true
   397  // if one exists. If none is found it returns a fallback location and false.
   398  func (s *Snapshot) matchErrorToModule(pm *ParsedModule, goCmdError string) (protocol.Location, bool, error) {
   399  	var reference *modfile.Line
   400  	matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1)
   401  
   402  	for i := len(matches) - 1; i >= 0; i-- {
   403  		ver := module.Version{Path: matches[i][1], Version: matches[i][2]}
   404  		if err := module.Check(ver.Path, ver.Version); err != nil {
   405  			continue
   406  		}
   407  		reference = findModuleReference(pm.File, ver)
   408  		if reference != nil {
   409  			break
   410  		}
   411  	}
   412  
   413  	if reference == nil {
   414  		// No match for the module path was found in the go.mod file.
   415  		// Show the error on the module declaration, if one exists, or
   416  		// just the first line of the file.
   417  		var start, end int
   418  		if pm.File.Module != nil && pm.File.Module.Syntax != nil {
   419  			syntax := pm.File.Module.Syntax
   420  			start, end = syntax.Start.Byte, syntax.End.Byte
   421  		}
   422  		loc, err := pm.Mapper.OffsetLocation(start, end)
   423  		return loc, false, err
   424  	}
   425  
   426  	loc, err := pm.Mapper.OffsetLocation(reference.Start.Byte, reference.End.Byte)
   427  	return loc, true, err
   428  }
   429  
   430  // goCommandDiagnostic creates a diagnostic for a given go command error.
   431  func (s *Snapshot) goCommandDiagnostic(pm *ParsedModule, loc protocol.Location, goCmdError string) (*Diagnostic, error) {
   432  	matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1)
   433  	var innermost *module.Version
   434  	for i := len(matches) - 1; i >= 0; i-- {
   435  		ver := module.Version{Path: matches[i][1], Version: matches[i][2]}
   436  		if err := module.Check(ver.Path, ver.Version); err != nil {
   437  			continue
   438  		}
   439  		innermost = &ver
   440  		break
   441  	}
   442  
   443  	switch {
   444  	case strings.Contains(goCmdError, "inconsistent vendoring"):
   445  		cmd, err := command.NewVendorCommand("Run go mod vendor", command.URIArg{URI: pm.URI})
   446  		if err != nil {
   447  			return nil, err
   448  		}
   449  		return &Diagnostic{
   450  			URI:      pm.URI,
   451  			Range:    loc.Range,
   452  			Severity: protocol.SeverityError,
   453  			Source:   ListError,
   454  			Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
   455  See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
   456  			SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   457  		}, nil
   458  
   459  	case strings.Contains(goCmdError, "updates to go.sum needed"), strings.Contains(goCmdError, "missing go.sum entry"):
   460  		var args []protocol.DocumentURI
   461  		args = append(args, s.View().ModFiles()...)
   462  		tidyCmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args})
   463  		if err != nil {
   464  			return nil, err
   465  		}
   466  		updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args})
   467  		if err != nil {
   468  			return nil, err
   469  		}
   470  		msg := "go.sum is out of sync with go.mod. Please update it by applying the quick fix."
   471  		if innermost != nil {
   472  			msg = fmt.Sprintf("go.sum is out of sync with go.mod: entry for %v is missing. Please updating it by applying the quick fix.", innermost)
   473  		}
   474  		return &Diagnostic{
   475  			URI:      pm.URI,
   476  			Range:    loc.Range,
   477  			Severity: protocol.SeverityError,
   478  			Source:   ListError,
   479  			Message:  msg,
   480  			SuggestedFixes: []SuggestedFix{
   481  				SuggestedFixFromCommand(tidyCmd, protocol.QuickFix),
   482  				SuggestedFixFromCommand(updateCmd, protocol.QuickFix),
   483  			},
   484  		}, nil
   485  	case strings.Contains(goCmdError, "disabled by GOPROXY=off") && innermost != nil:
   486  		title := fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version)
   487  		cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
   488  			URI:        pm.URI,
   489  			AddRequire: false,
   490  			GoCmdArgs:  []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)},
   491  		})
   492  		if err != nil {
   493  			return nil, err
   494  		}
   495  		return &Diagnostic{
   496  			URI:            pm.URI,
   497  			Range:          loc.Range,
   498  			Severity:       protocol.SeverityError,
   499  			Message:        fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version),
   500  			Source:         ListError,
   501  			SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   502  		}, nil
   503  	default:
   504  		return &Diagnostic{
   505  			URI:      pm.URI,
   506  			Range:    loc.Range,
   507  			Severity: protocol.SeverityError,
   508  			Source:   ListError,
   509  			Message:  goCmdError,
   510  		}, nil
   511  	}
   512  }
   513  
   514  func findModuleReference(mf *modfile.File, ver module.Version) *modfile.Line {
   515  	for _, req := range mf.Require {
   516  		if req.Mod == ver {
   517  			return req.Syntax
   518  		}
   519  	}
   520  	for _, ex := range mf.Exclude {
   521  		if ex.Mod == ver {
   522  			return ex.Syntax
   523  		}
   524  	}
   525  	for _, rep := range mf.Replace {
   526  		if rep.New == ver || rep.Old == ver {
   527  			return rep.Syntax
   528  		}
   529  	}
   530  	return nil
   531  }