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

     1  // Copyright 2020 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  	"go/ast"
    12  	"go/token"
    13  	"os"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"golang.org/x/mod/modfile"
    19  	"golang.org/x/tools/gopls/internal/file"
    20  	"golang.org/x/tools/gopls/internal/protocol/command"
    21  	"golang.org/x/tools/gopls/internal/protocol"
    22  	"golang.org/x/tools/internal/diff"
    23  	"golang.org/x/tools/internal/event"
    24  	"golang.org/x/tools/internal/event/tag"
    25  	"golang.org/x/tools/internal/gocommand"
    26  	"golang.org/x/tools/internal/memoize"
    27  )
    28  
    29  // This error is sought by mod diagnostics.
    30  var ErrNoModOnDisk = errors.New("go.mod file is not on disk")
    31  
    32  // A TidiedModule contains the results of running `go mod tidy` on a module.
    33  type TidiedModule struct {
    34  	// Diagnostics representing changes made by `go mod tidy`.
    35  	Diagnostics []*Diagnostic
    36  	// The bytes of the go.mod file after it was tidied.
    37  	TidiedContent []byte
    38  }
    39  
    40  // ModTidy returns the go.mod file that would be obtained by running
    41  // "go mod tidy". Concurrent requests are combined into a single command.
    42  func (s *Snapshot) ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error) {
    43  	ctx, done := event.Start(ctx, "cache.snapshot.ModTidy")
    44  	defer done()
    45  
    46  	uri := pm.URI
    47  	if pm.File == nil {
    48  		return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri)
    49  	}
    50  
    51  	s.mu.Lock()
    52  	entry, hit := s.modTidyHandles.Get(uri)
    53  	s.mu.Unlock()
    54  
    55  	type modTidyResult struct {
    56  		tidied *TidiedModule
    57  		err    error
    58  	}
    59  
    60  	// Cache miss?
    61  	if !hit {
    62  		// If the file handle is an overlay, it may not be written to disk.
    63  		// The go.mod file has to be on disk for `go mod tidy` to work.
    64  		// TODO(rfindley): is this still true with Go 1.16 overlay support?
    65  		fh, err := s.ReadFile(ctx, pm.URI)
    66  		if err != nil {
    67  			return nil, err
    68  		}
    69  		if _, ok := fh.(*overlay); ok {
    70  			if info, _ := os.Stat(uri.Path()); info == nil {
    71  				return nil, ErrNoModOnDisk
    72  			}
    73  		}
    74  
    75  		if err := s.awaitLoaded(ctx); err != nil {
    76  			return nil, err
    77  		}
    78  
    79  		handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} {
    80  			tidied, err := modTidyImpl(ctx, arg.(*Snapshot), uri.Path(), pm)
    81  			return modTidyResult{tidied, err}
    82  		})
    83  
    84  		entry = handle
    85  		s.mu.Lock()
    86  		s.modTidyHandles.Set(uri, entry, nil)
    87  		s.mu.Unlock()
    88  	}
    89  
    90  	// Await result.
    91  	v, err := s.awaitPromise(ctx, entry)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	res := v.(modTidyResult)
    96  	return res.tidied, res.err
    97  }
    98  
    99  // modTidyImpl runs "go mod tidy" on a go.mod file.
   100  func modTidyImpl(ctx context.Context, snapshot *Snapshot, filename string, pm *ParsedModule) (*TidiedModule, error) {
   101  	ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename))
   102  	defer done()
   103  
   104  	inv := &gocommand.Invocation{
   105  		Verb:       "mod",
   106  		Args:       []string{"tidy"},
   107  		WorkingDir: filepath.Dir(filename),
   108  	}
   109  	// TODO(adonovan): ensure that unsaved overlays are passed through to 'go'.
   110  	tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, WriteTemporaryModFile, inv)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	// Keep the temporary go.mod file around long enough to parse it.
   115  	defer cleanup()
   116  
   117  	if _, err := snapshot.view.gocmdRunner.Run(ctx, *inv); err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	// Go directly to disk to get the temporary mod file,
   122  	// since it is always on disk.
   123  	tempContents, err := os.ReadFile(tmpURI.Path())
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	ideal, err := modfile.Parse(tmpURI.Path(), tempContents, nil)
   128  	if err != nil {
   129  		// We do not need to worry about the temporary file's parse errors
   130  		// since it has been "tidied".
   131  		return nil, err
   132  	}
   133  
   134  	// Compare the original and tidied go.mod files to compute errors and
   135  	// suggested fixes.
   136  	diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	return &TidiedModule{
   142  		Diagnostics:   diagnostics,
   143  		TidiedContent: tempContents,
   144  	}, nil
   145  }
   146  
   147  // modTidyDiagnostics computes the differences between the original and tidied
   148  // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
   149  // may appear on the Go files that import packages from missing modules.
   150  func modTidyDiagnostics(ctx context.Context, snapshot *Snapshot, pm *ParsedModule, ideal *modfile.File) (diagnostics []*Diagnostic, err error) {
   151  	// First, determine which modules are unused and which are missing from the
   152  	// original go.mod file.
   153  	var (
   154  		unused          = make(map[string]*modfile.Require, len(pm.File.Require))
   155  		missing         = make(map[string]*modfile.Require, len(ideal.Require))
   156  		wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
   157  	)
   158  	for _, req := range pm.File.Require {
   159  		unused[req.Mod.Path] = req
   160  	}
   161  	for _, req := range ideal.Require {
   162  		origReq := unused[req.Mod.Path]
   163  		if origReq == nil {
   164  			missing[req.Mod.Path] = req
   165  			continue
   166  		} else if origReq.Indirect != req.Indirect {
   167  			wrongDirectness[req.Mod.Path] = origReq
   168  		}
   169  		delete(unused, req.Mod.Path)
   170  	}
   171  	for _, req := range wrongDirectness {
   172  		// Handle dependencies that are incorrectly labeled indirect and
   173  		// vice versa.
   174  		srcDiag, err := directnessDiagnostic(pm.Mapper, req)
   175  		if err != nil {
   176  			// We're probably in a bad state if we can't compute a
   177  			// directnessDiagnostic, but try to keep going so as to not suppress
   178  			// other, valid diagnostics.
   179  			event.Error(ctx, "computing directness diagnostic", err)
   180  			continue
   181  		}
   182  		diagnostics = append(diagnostics, srcDiag)
   183  	}
   184  	// Next, compute any diagnostics for modules that are missing from the
   185  	// go.mod file. The fixes will be for the go.mod file, but the
   186  	// diagnostics should also appear in both the go.mod file and the import
   187  	// statements in the Go files in which the dependencies are used.
   188  	// Finally, add errors for any unused dependencies.
   189  	if len(missing) > 0 {
   190  		missingModuleDiagnostics, err := missingModuleDiagnostics(ctx, snapshot, pm, ideal, missing)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  		diagnostics = append(diagnostics, missingModuleDiagnostics...)
   195  	}
   196  
   197  	// Opt: if this is the only diagnostic, we can avoid textual edits and just
   198  	// run the Go command.
   199  	//
   200  	// See also the documentation for command.RemoveDependencyArgs.OnlyDiagnostic.
   201  	onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
   202  	for _, req := range unused {
   203  		srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
   204  		if err != nil {
   205  			return nil, err
   206  		}
   207  		diagnostics = append(diagnostics, srcErr)
   208  	}
   209  	return diagnostics, nil
   210  }
   211  
   212  func missingModuleDiagnostics(ctx context.Context, snapshot *Snapshot, pm *ParsedModule, ideal *modfile.File, missing map[string]*modfile.Require) ([]*Diagnostic, error) {
   213  	missingModuleFixes := map[*modfile.Require][]SuggestedFix{}
   214  	var diagnostics []*Diagnostic
   215  	for _, req := range missing {
   216  		srcDiag, err := missingModuleDiagnostic(pm, req)
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  		missingModuleFixes[req] = srcDiag.SuggestedFixes
   221  		diagnostics = append(diagnostics, srcDiag)
   222  	}
   223  
   224  	// Add diagnostics for missing modules anywhere they are imported in the
   225  	// workspace.
   226  	metas, err := snapshot.WorkspaceMetadata(ctx)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  	// TODO(adonovan): opt: opportunities for parallelism abound.
   231  	for _, mp := range metas {
   232  		// Read both lists of files of this package.
   233  		//
   234  		// Parallelism is not necessary here as the files will have already been
   235  		// pre-read at load time.
   236  		goFiles, err := readFiles(ctx, snapshot, mp.GoFiles)
   237  		if err != nil {
   238  			return nil, err
   239  		}
   240  		compiledGoFiles, err := readFiles(ctx, snapshot, mp.CompiledGoFiles)
   241  		if err != nil {
   242  			return nil, err
   243  		}
   244  
   245  		missingImports := map[string]*modfile.Require{}
   246  
   247  		// If -mod=readonly is not set we may have successfully imported
   248  		// packages from missing modules. Otherwise they'll be in
   249  		// MissingDependencies. Combine both.
   250  		imps, err := parseImports(ctx, snapshot, goFiles)
   251  		if err != nil {
   252  			return nil, err
   253  		}
   254  		for imp := range imps {
   255  			if req, ok := missing[imp]; ok {
   256  				missingImports[imp] = req
   257  				break
   258  			}
   259  			// If the import is a package of the dependency, then add the
   260  			// package to the map, this will eliminate the need to do this
   261  			// prefix package search on each import for each file.
   262  			// Example:
   263  			//
   264  			// import (
   265  			//   "golang.org/x/tools/go/expect"
   266  			//   "golang.org/x/tools/go/packages"
   267  			// )
   268  			// They both are related to the same module: "golang.org/x/tools".
   269  			var match string
   270  			for _, req := range ideal.Require {
   271  				if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
   272  					match = req.Mod.Path
   273  				}
   274  			}
   275  			if req, ok := missing[match]; ok {
   276  				missingImports[imp] = req
   277  			}
   278  		}
   279  		// None of this package's imports are from missing modules.
   280  		if len(missingImports) == 0 {
   281  			continue
   282  		}
   283  		for _, goFile := range compiledGoFiles {
   284  			pgf, err := snapshot.ParseGo(ctx, goFile, ParseHeader)
   285  			if err != nil {
   286  				continue
   287  			}
   288  			file, m := pgf.File, pgf.Mapper
   289  			if file == nil || m == nil {
   290  				continue
   291  			}
   292  			imports := make(map[string]*ast.ImportSpec)
   293  			for _, imp := range file.Imports {
   294  				if imp.Path == nil {
   295  					continue
   296  				}
   297  				if target, err := strconv.Unquote(imp.Path.Value); err == nil {
   298  					imports[target] = imp
   299  				}
   300  			}
   301  			if len(imports) == 0 {
   302  				continue
   303  			}
   304  			for importPath, req := range missingImports {
   305  				imp, ok := imports[importPath]
   306  				if !ok {
   307  					continue
   308  				}
   309  				fixes, ok := missingModuleFixes[req]
   310  				if !ok {
   311  					return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
   312  				}
   313  				srcErr, err := missingModuleForImport(pgf, imp, req, fixes)
   314  				if err != nil {
   315  					return nil, err
   316  				}
   317  				diagnostics = append(diagnostics, srcErr)
   318  			}
   319  		}
   320  	}
   321  	return diagnostics, nil
   322  }
   323  
   324  // unusedDiagnostic returns a Diagnostic for an unused require.
   325  func unusedDiagnostic(m *protocol.Mapper, req *modfile.Require, onlyDiagnostic bool) (*Diagnostic, error) {
   326  	rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  	title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path)
   331  	cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{
   332  		URI:            m.URI,
   333  		OnlyDiagnostic: onlyDiagnostic,
   334  		ModulePath:     req.Mod.Path,
   335  	})
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	return &Diagnostic{
   340  		URI:            m.URI,
   341  		Range:          rng,
   342  		Severity:       protocol.SeverityWarning,
   343  		Source:         ModTidyError,
   344  		Message:        fmt.Sprintf("%s is not used in this module", req.Mod.Path),
   345  		SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   346  	}, nil
   347  }
   348  
   349  // directnessDiagnostic extracts errors when a dependency is labeled indirect when
   350  // it should be direct and vice versa.
   351  func directnessDiagnostic(m *protocol.Mapper, req *modfile.Require) (*Diagnostic, error) {
   352  	rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	direction := "indirect"
   357  	if req.Indirect {
   358  		direction = "direct"
   359  
   360  		// If the dependency should be direct, just highlight the // indirect.
   361  		if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
   362  			end := comments.Suffix[0].Start
   363  			end.LineRune += len(comments.Suffix[0].Token)
   364  			end.Byte += len(comments.Suffix[0].Token)
   365  			rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte)
   366  			if err != nil {
   367  				return nil, err
   368  			}
   369  		}
   370  	}
   371  	// If the dependency should be indirect, add the // indirect.
   372  	edits, err := switchDirectness(req, m)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	return &Diagnostic{
   377  		URI:      m.URI,
   378  		Range:    rng,
   379  		Severity: protocol.SeverityWarning,
   380  		Source:   ModTidyError,
   381  		Message:  fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
   382  		SuggestedFixes: []SuggestedFix{{
   383  			Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
   384  			Edits: map[protocol.DocumentURI][]protocol.TextEdit{
   385  				m.URI: edits,
   386  			},
   387  			ActionKind: protocol.QuickFix,
   388  		}},
   389  	}, nil
   390  }
   391  
   392  func missingModuleDiagnostic(pm *ParsedModule, req *modfile.Require) (*Diagnostic, error) {
   393  	var rng protocol.Range
   394  	// Default to the start of the file if there is no module declaration.
   395  	if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
   396  		start, end := pm.File.Module.Syntax.Span()
   397  		var err error
   398  		rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte)
   399  		if err != nil {
   400  			return nil, err
   401  		}
   402  	}
   403  	title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path)
   404  	cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
   405  		URI:        pm.Mapper.URI,
   406  		AddRequire: !req.Indirect,
   407  		GoCmdArgs:  []string{req.Mod.Path + "@" + req.Mod.Version},
   408  	})
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  	return &Diagnostic{
   413  		URI:            pm.Mapper.URI,
   414  		Range:          rng,
   415  		Severity:       protocol.SeverityError,
   416  		Source:         ModTidyError,
   417  		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   418  		SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   419  	}, nil
   420  }
   421  
   422  // switchDirectness gets the edits needed to change an indirect dependency to
   423  // direct and vice versa.
   424  func switchDirectness(req *modfile.Require, m *protocol.Mapper) ([]protocol.TextEdit, error) {
   425  	// We need a private copy of the parsed go.mod file, since we're going to
   426  	// modify it.
   427  	copied, err := modfile.Parse("", m.Content, nil)
   428  	if err != nil {
   429  		return nil, err
   430  	}
   431  	// Change the directness in the matching require statement. To avoid
   432  	// reordering the require statements, rewrite all of them.
   433  	var requires []*modfile.Require
   434  	seenVersions := make(map[string]string)
   435  	for _, r := range copied.Require {
   436  		if seen := seenVersions[r.Mod.Path]; seen != "" && seen != r.Mod.Version {
   437  			// Avoid a panic in SetRequire below, which panics on conflicting
   438  			// versions.
   439  			return nil, fmt.Errorf("%q has conflicting versions: %q and %q", r.Mod.Path, seen, r.Mod.Version)
   440  		}
   441  		seenVersions[r.Mod.Path] = r.Mod.Version
   442  		if r.Mod.Path == req.Mod.Path {
   443  			requires = append(requires, &modfile.Require{
   444  				Mod:      r.Mod,
   445  				Syntax:   r.Syntax,
   446  				Indirect: !r.Indirect,
   447  			})
   448  			continue
   449  		}
   450  		requires = append(requires, r)
   451  	}
   452  	copied.SetRequire(requires)
   453  	newContent, err := copied.Format()
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  	// Calculate the edits to be made due to the change.
   458  	edits := diff.Bytes(m.Content, newContent)
   459  	return protocol.EditsFromDiffEdits(m, edits)
   460  }
   461  
   462  // missingModuleForImport creates an error for a given import path that comes
   463  // from a missing module.
   464  func missingModuleForImport(pgf *ParsedGoFile, imp *ast.ImportSpec, req *modfile.Require, fixes []SuggestedFix) (*Diagnostic, error) {
   465  	if req.Syntax == nil {
   466  		return nil, fmt.Errorf("no syntax for %v", req)
   467  	}
   468  	rng, err := pgf.NodeRange(imp.Path)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  	return &Diagnostic{
   473  		URI:            pgf.URI,
   474  		Range:          rng,
   475  		Severity:       protocol.SeverityError,
   476  		Source:         ModTidyError,
   477  		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   478  		SuggestedFixes: fixes,
   479  	}, nil
   480  }
   481  
   482  // parseImports parses the headers of the specified files and returns
   483  // the set of strings that appear in import declarations within
   484  // GoFiles. Errors are ignored.
   485  //
   486  // (We can't simply use Metadata.Imports because it is based on
   487  // CompiledGoFiles, after cgo processing.)
   488  //
   489  // TODO(rfindley): this should key off ImportPath.
   490  func parseImports(ctx context.Context, s *Snapshot, files []file.Handle) (map[string]bool, error) {
   491  	pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), ParseHeader, false, files...)
   492  	if err != nil { // e.g. context cancellation
   493  		return nil, err
   494  	}
   495  
   496  	seen := make(map[string]bool)
   497  	for _, pgf := range pgfs {
   498  		for _, spec := range pgf.File.Imports {
   499  			path, _ := strconv.Unquote(spec.Path.Value)
   500  			seen[path] = true
   501  		}
   502  	}
   503  	return seen, nil
   504  }