aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/cache/mod_tidy.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/cache/mod_tidy.go')
-rw-r--r--gopls/internal/lsp/cache/mod_tidy.go469
1 files changed, 469 insertions, 0 deletions
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
new file mode 100644
index 000000000..0572e9d5c
--- /dev/null
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -0,0 +1,469 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "context"
+ "fmt"
+ "go/ast"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/event/tag"
+ "golang.org/x/tools/internal/gocommand"
+ "golang.org/x/tools/internal/memoize"
+)
+
+// ModTidy returns the go.mod file that would be obtained by running
+// "go mod tidy". Concurrent requests are combined into a single command.
+func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
+ uri := pm.URI
+ if pm.File == nil {
+ return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri)
+ }
+
+ s.mu.Lock()
+ entry, hit := s.modTidyHandles.Get(uri)
+ s.mu.Unlock()
+
+ type modTidyResult struct {
+ tidied *source.TidiedModule
+ err error
+ }
+
+ // Cache miss?
+ if !hit {
+ // If the file handle is an overlay, it may not be written to disk.
+ // The go.mod file has to be on disk for `go mod tidy` to work.
+ // TODO(rfindley): is this still true with Go 1.16 overlay support?
+ fh, err := s.GetFile(ctx, pm.URI)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := fh.(*Overlay); ok {
+ if info, _ := os.Stat(uri.Filename()); info == nil {
+ return nil, source.ErrNoModOnDisk
+ }
+ }
+
+ if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
+ return &source.TidiedModule{
+ Diagnostics: criticalErr.Diagnostics,
+ }, nil
+ }
+ if ctx.Err() != nil { // must check ctx after GetCriticalError
+ return nil, ctx.Err()
+ }
+
+ if err := s.awaitLoaded(ctx); err != nil {
+ return nil, err
+ }
+
+ handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} {
+ tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm)
+ return modTidyResult{tidied, err}
+ })
+
+ entry = handle
+ s.mu.Lock()
+ s.modTidyHandles.Set(uri, entry, nil)
+ s.mu.Unlock()
+ }
+
+ // Await result.
+ v, err := s.awaitPromise(ctx, entry.(*memoize.Promise))
+ if err != nil {
+ return nil, err
+ }
+ res := v.(modTidyResult)
+ return res.tidied, res.err
+}
+
+// modTidyImpl runs "go mod tidy" on a go.mod file.
+func modTidyImpl(ctx context.Context, snapshot *snapshot, filename string, pm *source.ParsedModule) (*source.TidiedModule, error) {
+ ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename))
+ defer done()
+
+ inv := &gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"tidy"},
+ WorkingDir: filepath.Dir(filename),
+ }
+ // TODO(adonovan): ensure that unsaved overlays are passed through to 'go'.
+ tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
+ if err != nil {
+ return nil, err
+ }
+ // Keep the temporary go.mod file around long enough to parse it.
+ defer cleanup()
+
+ if _, err := snapshot.view.gocmdRunner.Run(ctx, *inv); err != nil {
+ return nil, err
+ }
+
+ // Go directly to disk to get the temporary mod file,
+ // since it is always on disk.
+ tempContents, err := ioutil.ReadFile(tmpURI.Filename())
+ if err != nil {
+ return nil, err
+ }
+ ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
+ if err != nil {
+ // We do not need to worry about the temporary file's parse errors
+ // since it has been "tidied".
+ return nil, err
+ }
+
+ // Compare the original and tidied go.mod files to compute errors and
+ // suggested fixes.
+ diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal)
+ if err != nil {
+ return nil, err
+ }
+
+ return &source.TidiedModule{
+ Diagnostics: diagnostics,
+ TidiedContent: tempContents,
+ }, nil
+}
+
+// modTidyDiagnostics computes the differences between the original and tidied
+// go.mod files to produce diagnostic and suggested fixes. Some diagnostics
+// may appear on the Go files that import packages from missing modules.
+func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File) (diagnostics []*source.Diagnostic, err error) {
+ // First, determine which modules are unused and which are missing from the
+ // original go.mod file.
+ var (
+ unused = make(map[string]*modfile.Require, len(pm.File.Require))
+ missing = make(map[string]*modfile.Require, len(ideal.Require))
+ wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
+ )
+ for _, req := range pm.File.Require {
+ unused[req.Mod.Path] = req
+ }
+ for _, req := range ideal.Require {
+ origReq := unused[req.Mod.Path]
+ if origReq == nil {
+ missing[req.Mod.Path] = req
+ continue
+ } else if origReq.Indirect != req.Indirect {
+ wrongDirectness[req.Mod.Path] = origReq
+ }
+ delete(unused, req.Mod.Path)
+ }
+ for _, req := range wrongDirectness {
+ // Handle dependencies that are incorrectly labeled indirect and
+ // vice versa.
+ srcDiag, err := directnessDiagnostic(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
+ if err != nil {
+ // We're probably in a bad state if we can't compute a
+ // directnessDiagnostic, but try to keep going so as to not suppress
+ // other, valid diagnostics.
+ event.Error(ctx, "computing directness diagnostic", err)
+ continue
+ }
+ diagnostics = append(diagnostics, srcDiag)
+ }
+ // Next, compute any diagnostics for modules that are missing from the
+ // go.mod file. The fixes will be for the go.mod file, but the
+ // diagnostics should also appear in both the go.mod file and the import
+ // statements in the Go files in which the dependencies are used.
+ missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
+ for _, req := range missing {
+ srcDiag, err := missingModuleDiagnostic(pm, req)
+ if err != nil {
+ return nil, err
+ }
+ missingModuleFixes[req] = srcDiag.SuggestedFixes
+ diagnostics = append(diagnostics, srcDiag)
+ }
+ // Add diagnostics for missing modules anywhere they are imported in the
+ // workspace.
+ // TODO(adonovan): opt: opportunities for parallelism abound.
+ for _, m := range snapshot.workspaceMetadata() {
+ // Read both lists of files of this package, in parallel.
+ goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m)
+ if err != nil {
+ return nil, err
+ }
+
+ missingImports := map[string]*modfile.Require{}
+
+ // If -mod=readonly is not set we may have successfully imported
+ // packages from missing modules. Otherwise they'll be in
+ // MissingDependencies. Combine both.
+ imps, err := parseImports(ctx, snapshot, goFiles)
+ if err != nil {
+ return nil, err
+ }
+ for imp := range imps {
+ if req, ok := missing[imp]; ok {
+ missingImports[imp] = req
+ break
+ }
+ // If the import is a package of the dependency, then add the
+ // package to the map, this will eliminate the need to do this
+ // prefix package search on each import for each file.
+ // Example:
+ //
+ // import (
+ // "golang.org/x/tools/go/expect"
+ // "golang.org/x/tools/go/packages"
+ // )
+ // They both are related to the same module: "golang.org/x/tools".
+ var match string
+ for _, req := range ideal.Require {
+ if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
+ match = req.Mod.Path
+ }
+ }
+ if req, ok := missing[match]; ok {
+ missingImports[imp] = req
+ }
+ }
+ // None of this package's imports are from missing modules.
+ if len(missingImports) == 0 {
+ continue
+ }
+ for _, goFile := range compiledGoFiles {
+ pgf, err := snapshot.ParseGo(ctx, goFile, source.ParseHeader)
+ if err != nil {
+ continue
+ }
+ file, m := pgf.File, pgf.Mapper
+ if file == nil || m == nil {
+ continue
+ }
+ imports := make(map[string]*ast.ImportSpec)
+ for _, imp := range file.Imports {
+ if imp.Path == nil {
+ continue
+ }
+ if target, err := strconv.Unquote(imp.Path.Value); err == nil {
+ imports[target] = imp
+ }
+ }
+ if len(imports) == 0 {
+ continue
+ }
+ for importPath, req := range missingImports {
+ imp, ok := imports[importPath]
+ if !ok {
+ continue
+ }
+ fixes, ok := missingModuleFixes[req]
+ if !ok {
+ return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
+ }
+ srcErr, err := missingModuleForImport(pgf, imp, req, fixes)
+ if err != nil {
+ return nil, err
+ }
+ diagnostics = append(diagnostics, srcErr)
+ }
+ }
+ }
+ // Finally, add errors for any unused dependencies.
+ onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
+ for _, req := range unused {
+ srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
+ if err != nil {
+ return nil, err
+ }
+ diagnostics = append(diagnostics, srcErr)
+ }
+ return diagnostics, nil
+}
+
+// unusedDiagnostic returns a source.Diagnostic for an unused require.
+func unusedDiagnostic(m *protocol.Mapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) {
+ rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
+ if err != nil {
+ return nil, err
+ }
+ title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path)
+ cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{
+ URI: protocol.URIFromSpanURI(m.URI),
+ OnlyDiagnostic: onlyDiagnostic,
+ ModulePath: req.Mod.Path,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &source.Diagnostic{
+ URI: m.URI,
+ Range: rng,
+ Severity: protocol.SeverityWarning,
+ Source: source.ModTidyError,
+ Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path),
+ SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
+ }, nil
+}
+
+// directnessDiagnostic extracts errors when a dependency is labeled indirect when
+// it should be direct and vice versa.
+func directnessDiagnostic(m *protocol.Mapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) {
+ rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
+ if err != nil {
+ return nil, err
+ }
+ direction := "indirect"
+ if req.Indirect {
+ direction = "direct"
+
+ // If the dependency should be direct, just highlight the // indirect.
+ if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
+ end := comments.Suffix[0].Start
+ end.LineRune += len(comments.Suffix[0].Token)
+ end.Byte += len(comments.Suffix[0].Token)
+ rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ // If the dependency should be indirect, add the // indirect.
+ edits, err := switchDirectness(req, m, computeEdits)
+ if err != nil {
+ return nil, err
+ }
+ return &source.Diagnostic{
+ URI: m.URI,
+ Range: rng,
+ Severity: protocol.SeverityWarning,
+ Source: source.ModTidyError,
+ Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
+ SuggestedFixes: []source.SuggestedFix{{
+ Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
+ Edits: map[span.URI][]protocol.TextEdit{
+ m.URI: edits,
+ },
+ ActionKind: protocol.QuickFix,
+ }},
+ }, nil
+}
+
+func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*source.Diagnostic, error) {
+ var rng protocol.Range
+ // Default to the start of the file if there is no module declaration.
+ if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
+ start, end := pm.File.Module.Syntax.Span()
+ var err error
+ rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte)
+ if err != nil {
+ return nil, err
+ }
+ }
+ title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path)
+ cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
+ URI: protocol.URIFromSpanURI(pm.Mapper.URI),
+ AddRequire: !req.Indirect,
+ GoCmdArgs: []string{req.Mod.Path + "@" + req.Mod.Version},
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &source.Diagnostic{
+ URI: pm.Mapper.URI,
+ Range: rng,
+ Severity: protocol.SeverityError,
+ Source: source.ModTidyError,
+ Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
+ SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
+ }, nil
+}
+
+// switchDirectness gets the edits needed to change an indirect dependency to
+// direct and vice versa.
+func switchDirectness(req *modfile.Require, m *protocol.Mapper, computeEdits source.DiffFunction) ([]protocol.TextEdit, error) {
+ // We need a private copy of the parsed go.mod file, since we're going to
+ // modify it.
+ copied, err := modfile.Parse("", m.Content, nil)
+ if err != nil {
+ return nil, err
+ }
+ // Change the directness in the matching require statement. To avoid
+ // reordering the require statements, rewrite all of them.
+ var requires []*modfile.Require
+ seenVersions := make(map[string]string)
+ for _, r := range copied.Require {
+ if seen := seenVersions[r.Mod.Path]; seen != "" && seen != r.Mod.Version {
+ // Avoid a panic in SetRequire below, which panics on conflicting
+ // versions.
+ return nil, fmt.Errorf("%q has conflicting versions: %q and %q", r.Mod.Path, seen, r.Mod.Version)
+ }
+ seenVersions[r.Mod.Path] = r.Mod.Version
+ if r.Mod.Path == req.Mod.Path {
+ requires = append(requires, &modfile.Require{
+ Mod: r.Mod,
+ Syntax: r.Syntax,
+ Indirect: !r.Indirect,
+ })
+ continue
+ }
+ requires = append(requires, r)
+ }
+ copied.SetRequire(requires)
+ newContent, err := copied.Format()
+ if err != nil {
+ return nil, err
+ }
+ // Calculate the edits to be made due to the change.
+ edits := computeEdits(string(m.Content), string(newContent))
+ return source.ToProtocolEdits(m, edits)
+}
+
+// missingModuleForImport creates an error for a given import path that comes
+// from a missing module.
+func missingModuleForImport(pgf *source.ParsedGoFile, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) {
+ if req.Syntax == nil {
+ return nil, fmt.Errorf("no syntax for %v", req)
+ }
+ rng, err := pgf.NodeRange(imp.Path)
+ if err != nil {
+ return nil, err
+ }
+ return &source.Diagnostic{
+ URI: pgf.URI,
+ Range: rng,
+ Severity: protocol.SeverityError,
+ Source: source.ModTidyError,
+ Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
+ SuggestedFixes: fixes,
+ }, nil
+}
+
+// parseImports parses the headers of the specified files and returns
+// the set of strings that appear in import declarations within
+// GoFiles. Errors are ignored.
+//
+// (We can't simply use Metadata.Imports because it is based on
+// CompiledGoFiles, after cgo processing.)
+//
+// TODO(rfindley): this should key off source.ImportPath.
+func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) (map[string]bool, error) {
+ pgfs, _, err := s.parseCache.parseFiles(ctx, source.ParseHeader, files...)
+ if err != nil { // e.g. context cancellation
+ return nil, err
+ }
+
+ seen := make(map[string]bool)
+ for _, pgf := range pgfs {
+ for _, spec := range pgf.File.Imports {
+ path, _ := strconv.Unquote(spec.Path.Value)
+ seen[path] = true
+ }
+ }
+ return seen, nil
+}