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/lsp/debug/tag"
21 "golang.org/x/tools/internal/lsp/diff"
22 "golang.org/x/tools/internal/lsp/protocol"
23 "golang.org/x/tools/internal/lsp/source"
24 "golang.org/x/tools/internal/memoize"
25 "golang.org/x/tools/internal/span"
28 type modTidyKey struct {
31 gomod source.FileIdentity
33 unsavedOverlays string
37 type modTidyHandle struct {
38 handle *memoize.Handle
41 type modTidyData struct {
42 tidied *source.TidiedModule
46 func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) {
47 v, err := mth.handle.Get(ctx, snapshot.generation, snapshot)
51 data := v.(*modTidyData)
52 return data.tidied, data.err
55 func (s *snapshot) ModTidy(ctx context.Context, fh source.FileHandle) (*source.TidiedModule, error) {
56 if fh.Kind() != source.Mod {
57 return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
59 if s.workspaceMode()&tempModfile == 0 {
60 return nil, source.ErrTmpModfileUnsupported
62 if handle := s.getModTidyHandle(fh.URI()); handle != nil {
63 return handle.tidy(ctx, s)
65 // If the file handle is an overlay, it may not be written to disk.
66 // The go.mod file has to be on disk for `go mod tidy` to work.
67 if _, ok := fh.(*overlay); ok {
68 if info, _ := os.Stat(fh.URI().Filename()); info == nil {
69 return nil, source.ErrNoModOnDisk
72 workspacePkgs, err := s.WorkspacePackages(ctx)
76 importHash, err := hashImports(ctx, workspacePkgs)
82 overlayHash := hashUnsavedOverlays(s.files)
85 // Make sure to use the module root in the configuration.
86 cfg := s.config(ctx, filepath.Dir(fh.URI().Filename()))
88 sessionID: s.view.session.id,
89 view: s.view.folder.Filename(),
91 unsavedOverlays: overlayHash,
92 gomod: fh.FileIdentity(),
95 h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
96 ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
99 snapshot := arg.(*snapshot)
100 pm, err := snapshot.ParseMod(ctx, fh)
101 if err != nil || len(pm.ParseErrors) > 0 {
103 err = fmt.Errorf("could not parse module to tidy: %v", pm.ParseErrors)
105 var errors []source.Error
107 errors = pm.ParseErrors
110 tidied: &source.TidiedModule{
117 // Get a new config to avoid races, since it may be modified by
118 // goCommandInvocation.
119 cfg := s.config(ctx, filepath.Dir(fh.URI().Filename()))
120 tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, cfg, true, "mod", []string{"tidy"})
122 return &modTidyData{err: err}
124 // Keep the temporary go.mod file around long enough to parse it.
127 if _, err := runner.Run(ctx, *inv); err != nil {
128 return &modTidyData{err: err}
130 // Go directly to disk to get the temporary mod file, since it is
132 tempContents, err := ioutil.ReadFile(tmpURI.Filename())
134 return &modTidyData{err: err}
136 ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
138 // We do not need to worry about the temporary file's parse errors
139 // since it has been "tidied".
140 return &modTidyData{err: err}
142 // Compare the original and tidied go.mod files to compute errors and
144 errors, err := modTidyErrors(ctx, snapshot, pm, ideal, workspacePkgs)
146 return &modTidyData{err: err}
149 tidied: &source.TidiedModule{
152 TidiedContent: tempContents,
157 mth := &modTidyHandle{handle: h}
159 s.modTidyHandles[fh.URI()] = mth
162 return mth.tidy(ctx, s)
165 func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
166 results := make(map[string]bool)
168 for _, pkg := range wsPackages {
169 for _, path := range pkg.Imports() {
170 imp := path.PkgPath()
171 if _, ok := results[imp]; !ok {
173 imports = append(imports, imp)
177 sort.Strings(imports)
178 hashed := strings.Join(imports, ",")
179 return hashContents([]byte(hashed)), nil
182 // modTidyErrors computes the differences between the original and tidied
183 // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
184 // may appear on the Go files that import packages from missing modules.
185 func modTidyErrors(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []source.Package) (errors []source.Error, err error) {
186 // First, determine which modules are unused and which are missing from the
187 // original go.mod file.
189 unused = make(map[string]*modfile.Require, len(pm.File.Require))
190 missing = make(map[string]*modfile.Require, len(ideal.Require))
191 wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
193 for _, req := range pm.File.Require {
194 unused[req.Mod.Path] = req
196 for _, req := range ideal.Require {
197 origReq := unused[req.Mod.Path]
199 missing[req.Mod.Path] = req
201 } else if origReq.Indirect != req.Indirect {
202 wrongDirectness[req.Mod.Path] = origReq
204 delete(unused, req.Mod.Path)
206 for _, req := range unused {
207 srcErr, err := unusedError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
211 errors = append(errors, srcErr)
213 for _, req := range wrongDirectness {
214 // Handle dependencies that are incorrectly labeled indirect and
216 srcErr, err := directnessError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
220 errors = append(errors, srcErr)
222 // Next, compute any diagnostics for modules that are missing from the
223 // go.mod file. The fixes will be for the go.mod file, but the
224 // diagnostics should also appear in both the go.mod file and the import
225 // statements in the Go files in which the dependencies are used.
226 missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
227 for _, req := range missing {
228 srcErr, err := missingModuleError(snapshot, pm, req)
232 missingModuleFixes[req] = srcErr.SuggestedFixes
233 errors = append(errors, srcErr)
235 // Add diagnostics for missing modules anywhere they are imported in the
237 for _, pkg := range workspacePkgs {
238 missingImports := map[string]*modfile.Require{}
239 for _, imp := range pkg.Imports() {
240 if req, ok := missing[imp.PkgPath()]; ok {
241 missingImports[imp.PkgPath()] = req
244 // If the import is a package of the dependency, then add the
245 // package to the map, this will eliminate the need to do this
246 // prefix package search on each import for each file.
250 // "golang.org/x/tools/go/expect"
251 // "golang.org/x/tools/go/packages"
253 // They both are related to the same module: "golang.org/x/tools".
255 for _, req := range ideal.Require {
256 if strings.HasPrefix(imp.PkgPath(), req.Mod.Path) && len(req.Mod.Path) > len(match) {
260 if req, ok := missing[match]; ok {
261 missingImports[imp.PkgPath()] = req
264 // None of this package's imports are from missing modules.
265 if len(missingImports) == 0 {
268 for _, pgf := range pkg.CompiledGoFiles() {
269 file, m := pgf.File, pgf.Mapper
270 if file == nil || m == nil {
273 imports := make(map[string]*ast.ImportSpec)
274 for _, imp := range file.Imports {
278 if target, err := strconv.Unquote(imp.Path.Value); err == nil {
279 imports[target] = imp
282 if len(imports) == 0 {
285 for importPath, req := range missingImports {
286 imp, ok := imports[importPath]
290 fixes, ok := missingModuleFixes[req]
292 return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
294 srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes)
298 errors = append(errors, srcErr)
305 // unusedError returns a source.Error for an unused require.
306 func unusedError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (source.Error, error) {
307 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
309 return source.Error{}, err
311 edits, err := dropDependency(req, m, computeEdits)
313 return source.Error{}, err
316 Category: source.GoModTidy,
317 Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path),
320 SuggestedFixes: []source.SuggestedFix{{
321 Title: fmt.Sprintf("Remove dependency: %s", req.Mod.Path),
322 Edits: map[span.URI][]protocol.TextEdit{
329 // directnessError extracts errors when a dependency is labeled indirect when
330 // it should be direct and vice versa.
331 func directnessError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (source.Error, error) {
332 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
334 return source.Error{}, err
336 direction := "indirect"
340 // If the dependency should be direct, just highlight the // indirect.
341 if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
342 end := comments.Suffix[0].Start
343 end.LineRune += len(comments.Suffix[0].Token)
344 end.Byte += len([]byte(comments.Suffix[0].Token))
345 rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
347 return source.Error{}, err
351 // If the dependency should be indirect, add the // indirect.
352 edits, err := switchDirectness(req, m, computeEdits)
354 return source.Error{}, err
357 Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
360 Category: source.GoModTidy,
361 SuggestedFixes: []source.SuggestedFix{{
362 Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
363 Edits: map[span.URI][]protocol.TextEdit{
370 func missingModuleError(snapshot source.Snapshot, pm *source.ParsedModule, req *modfile.Require) (source.Error, error) {
371 start, end := pm.File.Module.Syntax.Span()
372 rng, err := rangeFromPositions(pm.Mapper, start, end)
374 return source.Error{}, err
376 edits, err := addRequireFix(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
378 return source.Error{}, err
380 fix := &source.SuggestedFix{
381 Title: fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path),
382 Edits: map[span.URI][]protocol.TextEdit{
383 pm.Mapper.URI: edits,
389 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
390 Category: source.GoModTidy,
391 Kind: source.ModTidyError,
392 SuggestedFixes: []source.SuggestedFix{*fix},
396 // dropDependency returns the edits to remove the given require from the go.mod
398 func dropDependency(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
399 // We need a private copy of the parsed go.mod file, since we're going to
401 copied, err := modfile.Parse("", m.Content, nil)
405 if err := copied.DropRequire(req.Mod.Path); err != nil {
409 newContent, err := copied.Format()
413 // Calculate the edits to be made due to the change.
414 diff := computeEdits(m.URI, string(m.Content), string(newContent))
415 return source.ToProtocolEdits(m, diff)
418 // switchDirectness gets the edits needed to change an indirect dependency to
419 // direct and vice versa.
420 func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
421 // We need a private copy of the parsed go.mod file, since we're going to
423 copied, err := modfile.Parse("", m.Content, nil)
427 // Change the directness in the matching require statement. To avoid
428 // reordering the require statements, rewrite all of them.
429 var requires []*modfile.Require
430 for _, r := range copied.Require {
431 if r.Mod.Path == req.Mod.Path {
432 requires = append(requires, &modfile.Require{
435 Indirect: !r.Indirect,
439 requires = append(requires, r)
441 copied.SetRequire(requires)
442 newContent, err := copied.Format()
446 // Calculate the edits to be made due to the change.
447 diff := computeEdits(m.URI, string(m.Content), string(newContent))
448 return source.ToProtocolEdits(m, diff)
451 // missingModuleForImport creates an error for a given import path that comes
452 // from a missing module.
453 func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (source.Error, error) {
454 if req.Syntax == nil {
455 return source.Error{}, fmt.Errorf("no syntax for %v", req)
457 spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
459 return source.Error{}, err
461 rng, err := m.Range(spn)
463 return source.Error{}, err
466 Category: source.GoModTidy,
469 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
470 Kind: source.ModTidyError,
471 SuggestedFixes: fixes,
475 // addRequireFix creates edits for adding a given require to a go.mod file.
476 func addRequireFix(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
477 // We need a private copy of the parsed go.mod file, since we're going to
479 copied, err := modfile.Parse("", m.Content, nil)
483 // Calculate the quick fix edits that need to be made to the go.mod file.
484 if err := copied.AddRequire(req.Mod.Path, req.Mod.Version); err != nil {
488 newContents, err := copied.Format()
492 // Calculate the edits to be made due to the change.
493 diff := computeEdits(m.URI, string(m.Content), string(newContents))
494 return source.ToProtocolEdits(m, diff)
497 func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
498 toPoint := func(offset int) (span.Point, error) {
499 l, c, err := m.Converter.ToPosition(offset)
501 return span.Point{}, err
503 return span.NewPoint(l, c, offset), nil
505 start, err := toPoint(s.Byte)
507 return protocol.Range{}, err
509 end, err := toPoint(e.Byte)
511 return protocol.Range{}, err
513 return m.Range(span.New(m.URI, start, end))