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.
18 "golang.org/x/mod/modfile"
19 "golang.org/x/tools/internal/event"
20 "golang.org/x/tools/internal/gocommand"
21 "golang.org/x/tools/internal/lsp/debug/tag"
22 "golang.org/x/tools/internal/lsp/diff"
23 "golang.org/x/tools/internal/lsp/protocol"
24 "golang.org/x/tools/internal/lsp/source"
25 "golang.org/x/tools/internal/memoize"
26 "golang.org/x/tools/internal/span"
29 type modTidyKey struct {
32 gomod source.FileIdentity
34 unsavedOverlays string
38 type modTidyHandle struct {
39 handle *memoize.Handle
42 type modTidyData struct {
43 tidied *source.TidiedModule
47 func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) {
48 v, err := mth.handle.Get(ctx, snapshot.generation, snapshot)
52 data := v.(*modTidyData)
53 return data.tidied, data.err
56 func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
58 return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI)
60 if handle := s.getModTidyHandle(pm.URI); handle != nil {
61 return handle.tidy(ctx, s)
63 fh, err := s.GetFile(ctx, pm.URI)
67 // If the file handle is an overlay, it may not be written to disk.
68 // The go.mod file has to be on disk for `go mod tidy` to work.
69 if _, ok := fh.(*overlay); ok {
70 if info, _ := os.Stat(fh.URI().Filename()); info == nil {
71 return nil, source.ErrNoModOnDisk
74 if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
75 return &source.TidiedModule{
76 Errors: criticalErr.ErrorList,
79 workspacePkgs, err := s.WorkspacePackages(ctx)
83 importHash, err := hashImports(ctx, workspacePkgs)
89 overlayHash := hashUnsavedOverlays(s.files)
93 sessionID: s.view.session.id,
94 view: s.view.folder.Filename(),
96 unsavedOverlays: overlayHash,
97 gomod: fh.FileIdentity(),
100 h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
101 ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
104 snapshot := arg.(*snapshot)
105 inv := &gocommand.Invocation{
107 Args: []string{"tidy"},
108 WorkingDir: filepath.Dir(fh.URI().Filename()),
110 tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile|source.AllowNetwork, inv)
112 return &modTidyData{err: err}
114 // Keep the temporary go.mod file around long enough to parse it.
117 if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
118 return &modTidyData{err: err}
120 // Go directly to disk to get the temporary mod file, since it is
122 tempContents, err := ioutil.ReadFile(tmpURI.Filename())
124 return &modTidyData{err: err}
126 ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
128 // We do not need to worry about the temporary file's parse errors
129 // since it has been "tidied".
130 return &modTidyData{err: err}
132 // Compare the original and tidied go.mod files to compute errors and
134 errors, err := modTidyErrors(ctx, snapshot, pm, ideal, workspacePkgs)
136 return &modTidyData{err: err}
139 tidied: &source.TidiedModule{
141 TidiedContent: tempContents,
146 mth := &modTidyHandle{handle: h}
148 s.modTidyHandles[fh.URI()] = mth
151 return mth.tidy(ctx, s)
154 func (s *snapshot) parseModError(ctx context.Context, fh source.FileHandle, errText string) *source.Error {
155 // Match on common error messages. This is really hacky, but I'm not sure
156 // of any better way. This can be removed when golang/go#39164 is resolved.
157 isInconsistentVendor := strings.Contains(errText, "inconsistent vendoring")
158 isGoSumUpdates := strings.Contains(errText, "updates to go.sum needed") || strings.Contains(errText, "missing go.sum entry")
160 if !isInconsistentVendor && !isGoSumUpdates {
164 pmf, err := s.ParseMod(ctx, fh)
168 if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
171 rng, err := rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
175 args, err := source.MarshalArgs(protocol.URIFromSpanURI(fh.URI()))
181 case isInconsistentVendor:
182 return &source.Error{
185 Kind: source.ListError,
186 Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
187 See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
188 SuggestedFixes: []source.SuggestedFix{{
189 Title: source.CommandVendor.Title,
190 Command: &protocol.Command{
191 Command: source.CommandVendor.ID(),
192 Title: source.CommandVendor.Title,
199 return &source.Error{
202 Kind: source.ListError,
203 Message: `go.sum is out of sync with go.mod. Please update it or run "go mod tidy".`,
204 SuggestedFixes: []source.SuggestedFix{
206 Title: source.CommandTidy.Title,
207 Command: &protocol.Command{
208 Command: source.CommandTidy.ID(),
209 Title: source.CommandTidy.Title,
214 Title: source.CommandUpdateGoSum.Title,
215 Command: &protocol.Command{
216 Command: source.CommandUpdateGoSum.ID(),
217 Title: source.CommandUpdateGoSum.Title,
227 func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
228 results := make(map[string]bool)
230 for _, pkg := range wsPackages {
231 for _, path := range pkg.Imports() {
232 imp := path.PkgPath()
233 if _, ok := results[imp]; !ok {
235 imports = append(imports, imp)
238 imports = append(imports, pkg.MissingDependencies()...)
240 sort.Strings(imports)
241 hashed := strings.Join(imports, ",")
242 return hashContents([]byte(hashed)), nil
245 // modTidyErrors computes the differences between the original and tidied
246 // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
247 // may appear on the Go files that import packages from missing modules.
248 func modTidyErrors(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []source.Package) (errors []*source.Error, err error) {
249 // First, determine which modules are unused and which are missing from the
250 // original go.mod file.
252 unused = make(map[string]*modfile.Require, len(pm.File.Require))
253 missing = make(map[string]*modfile.Require, len(ideal.Require))
254 wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
256 for _, req := range pm.File.Require {
257 unused[req.Mod.Path] = req
259 for _, req := range ideal.Require {
260 origReq := unused[req.Mod.Path]
262 missing[req.Mod.Path] = req
264 } else if origReq.Indirect != req.Indirect {
265 wrongDirectness[req.Mod.Path] = origReq
267 delete(unused, req.Mod.Path)
269 for _, req := range wrongDirectness {
270 // Handle dependencies that are incorrectly labeled indirect and
272 srcErr, err := directnessError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
276 errors = append(errors, srcErr)
278 // Next, compute any diagnostics for modules that are missing from the
279 // go.mod file. The fixes will be for the go.mod file, but the
280 // diagnostics should also appear in both the go.mod file and the import
281 // statements in the Go files in which the dependencies are used.
282 missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
283 for _, req := range missing {
284 srcErr, err := missingModuleError(snapshot, pm, req)
288 missingModuleFixes[req] = srcErr.SuggestedFixes
289 errors = append(errors, srcErr)
291 // Add diagnostics for missing modules anywhere they are imported in the
293 for _, pkg := range workspacePkgs {
294 missingImports := map[string]*modfile.Require{}
295 var importedPkgs []string
297 // If -mod=readonly is not set we may have successfully imported
298 // packages from missing modules. Otherwise they'll be in
299 // MissingDependencies. Combine both.
300 for _, imp := range pkg.Imports() {
301 importedPkgs = append(importedPkgs, imp.PkgPath())
303 importedPkgs = append(importedPkgs, pkg.MissingDependencies()...)
305 for _, imp := range importedPkgs {
306 if req, ok := missing[imp]; ok {
307 missingImports[imp] = req
310 // If the import is a package of the dependency, then add the
311 // package to the map, this will eliminate the need to do this
312 // prefix package search on each import for each file.
316 // "golang.org/x/tools/go/expect"
317 // "golang.org/x/tools/go/packages"
319 // They both are related to the same module: "golang.org/x/tools".
321 for _, req := range ideal.Require {
322 if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
326 if req, ok := missing[match]; ok {
327 missingImports[imp] = req
330 // None of this package's imports are from missing modules.
331 if len(missingImports) == 0 {
334 for _, pgf := range pkg.CompiledGoFiles() {
335 file, m := pgf.File, pgf.Mapper
336 if file == nil || m == nil {
339 imports := make(map[string]*ast.ImportSpec)
340 for _, imp := range file.Imports {
344 if target, err := strconv.Unquote(imp.Path.Value); err == nil {
345 imports[target] = imp
348 if len(imports) == 0 {
351 for importPath, req := range missingImports {
352 imp, ok := imports[importPath]
356 fixes, ok := missingModuleFixes[req]
358 return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
360 srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes)
364 errors = append(errors, srcErr)
368 // Finally, add errors for any unused dependencies.
369 onlyError := len(errors) == 0 && len(unused) == 1
370 for _, req := range unused {
371 srcErr, err := unusedError(pm.Mapper, req, onlyError, snapshot.View().Options().ComputeEdits)
375 errors = append(errors, srcErr)
380 // unusedError returns a source.Error for an unused require.
381 func unusedError(m *protocol.ColumnMapper, req *modfile.Require, onlyError bool, computeEdits diff.ComputeEdits) (*source.Error, error) {
382 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
386 args, err := source.MarshalArgs(m.URI, onlyError, req.Mod.Path)
390 return &source.Error{
391 Category: source.GoModTidy,
392 Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path),
395 SuggestedFixes: []source.SuggestedFix{{
396 Title: fmt.Sprintf("Remove dependency: %s", req.Mod.Path),
397 Command: &protocol.Command{
398 Title: source.CommandRemoveDependency.Title,
399 Command: source.CommandRemoveDependency.ID(),
406 // directnessError extracts errors when a dependency is labeled indirect when
407 // it should be direct and vice versa.
408 func directnessError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Error, error) {
409 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
413 direction := "indirect"
417 // If the dependency should be direct, just highlight the // indirect.
418 if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
419 end := comments.Suffix[0].Start
420 end.LineRune += len(comments.Suffix[0].Token)
421 end.Byte += len([]byte(comments.Suffix[0].Token))
422 rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
428 // If the dependency should be indirect, add the // indirect.
429 edits, err := switchDirectness(req, m, computeEdits)
433 return &source.Error{
434 Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
437 Category: source.GoModTidy,
438 SuggestedFixes: []source.SuggestedFix{{
439 Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
440 Edits: map[span.URI][]protocol.TextEdit{
447 func missingModuleError(snapshot source.Snapshot, pm *source.ParsedModule, req *modfile.Require) (*source.Error, error) {
448 var rng protocol.Range
449 // Default to the start of the file if there is no module declaration.
450 if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
451 start, end := pm.File.Module.Syntax.Span()
453 rng, err = rangeFromPositions(pm.Mapper, start, end)
458 args, err := source.MarshalArgs(pm.Mapper.URI, !req.Indirect, []string{req.Mod.Path + "@" + req.Mod.Version})
462 return &source.Error{
465 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
466 Category: source.GoModTidy,
467 Kind: source.ModTidyError,
468 SuggestedFixes: []source.SuggestedFix{{
469 Title: fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path),
470 Command: &protocol.Command{
471 Title: source.CommandAddDependency.Title,
472 Command: source.CommandAddDependency.ID(),
479 // switchDirectness gets the edits needed to change an indirect dependency to
480 // direct and vice versa.
481 func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
482 // We need a private copy of the parsed go.mod file, since we're going to
484 copied, err := modfile.Parse("", m.Content, nil)
488 // Change the directness in the matching require statement. To avoid
489 // reordering the require statements, rewrite all of them.
490 var requires []*modfile.Require
491 for _, r := range copied.Require {
492 if r.Mod.Path == req.Mod.Path {
493 requires = append(requires, &modfile.Require{
496 Indirect: !r.Indirect,
500 requires = append(requires, r)
502 copied.SetRequire(requires)
503 newContent, err := copied.Format()
507 // Calculate the edits to be made due to the change.
508 diff, err := computeEdits(m.URI, string(m.Content), string(newContent))
512 return source.ToProtocolEdits(m, diff)
515 // missingModuleForImport creates an error for a given import path that comes
516 // from a missing module.
517 func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Error, error) {
518 if req.Syntax == nil {
519 return nil, fmt.Errorf("no syntax for %v", req)
521 spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
525 rng, err := m.Range(spn)
529 return &source.Error{
530 Category: source.GoModTidy,
533 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
534 Kind: source.ModTidyError,
535 SuggestedFixes: fixes,
539 func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
540 toPoint := func(offset int) (span.Point, error) {
541 l, c, err := m.Converter.ToPosition(offset)
543 return span.Point{}, err
545 return span.NewPoint(l, c, offset), nil
547 start, err := toPoint(s.Byte)
549 return protocol.Range{}, err
551 end, err := toPoint(e.Byte)
553 return protocol.Range{}, err
555 return m.Range(span.New(m.URI, start, end))