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