|  | // 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" | 
|  | "go/token" | 
|  | "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 _, id := range snapshot.workspacePackageIDs() { | 
|  | m := snapshot.getMetadata(id) | 
|  | if m == nil { | 
|  | return nil, fmt.Errorf("no metadata for %s", id) | 
|  | } | 
|  |  | 
|  | // 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. | 
|  | for imp := range parseImports(ctx, snapshot, goFiles) { | 
|  | 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.Tok, m, 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.ColumnMapper, 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.ColumnMapper, 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.ColumnMapper, 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(file *token.File, m *protocol.ColumnMapper, 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 := m.PosRange(imp.Path.Pos(), imp.Path.End()) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | return &source.Diagnostic{ | 
|  | URI:            m.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 | 
|  | } | 
|  |  | 
|  | func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) { | 
|  | toPoint := func(offset int) (span.Point, error) { | 
|  | l, c, err := span.ToPosition(m.TokFile, offset) | 
|  | if err != nil { | 
|  | return span.Point{}, err | 
|  | } | 
|  | return span.NewPoint(l, c, offset), nil | 
|  | } | 
|  | start, err := toPoint(s.Byte) | 
|  | if err != nil { | 
|  | return span.Span{}, err | 
|  | } | 
|  | end, err := toPoint(e.Byte) | 
|  | if err != nil { | 
|  | return span.Span{}, err | 
|  | } | 
|  | return span.New(m.URI, start, end), 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.) | 
|  | func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) map[string]bool { | 
|  | s.mu.Lock() // peekOrParse requires a locked snapshot (!) | 
|  | defer s.mu.Unlock() | 
|  | seen := make(map[string]bool) | 
|  | for _, file := range files { | 
|  | f, err := peekOrParse(ctx, s, file, source.ParseHeader) | 
|  | if err != nil { | 
|  | continue | 
|  | } | 
|  | for _, spec := range f.File.Imports { | 
|  | path, _ := strconv.Unquote(spec.Path.Value) | 
|  | seen[path] = true | 
|  | } | 
|  | } | 
|  | return seen | 
|  | } |