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, fh source.FileHandle) (*source.TidiedModule, error) {
57 if fh.Kind() != source.Mod {
58 return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
60 if handle := s.getModTidyHandle(fh.URI()); handle != nil {
61 return handle.tidy(ctx, s)
63 // If the file handle is an overlay, it may not be written to disk.
64 // The go.mod file has to be on disk for `go mod tidy` to work.
65 if _, ok := fh.(*overlay); ok {
66 if info, _ := os.Stat(fh.URI().Filename()); info == nil {
67 return nil, source.ErrNoModOnDisk
70 workspacePkgs, err := s.WorkspacePackages(ctx)
72 if tm, ok := s.parseModErrors(ctx, fh, err); ok {
77 importHash, err := hashImports(ctx, workspacePkgs)
83 overlayHash := hashUnsavedOverlays(s.files)
87 sessionID: s.view.session.id,
88 view: s.view.folder.Filename(),
90 unsavedOverlays: overlayHash,
91 gomod: fh.FileIdentity(),
94 h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
95 ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
98 snapshot := arg.(*snapshot)
99 pm, err := snapshot.ParseMod(ctx, fh)
100 if err != nil || len(pm.ParseErrors) > 0 {
102 err = fmt.Errorf("could not parse module to tidy: %v", pm.ParseErrors)
104 var errors []source.Error
106 errors = pm.ParseErrors
109 tidied: &source.TidiedModule{
116 inv := &gocommand.Invocation{
118 Args: []string{"tidy"},
119 WorkingDir: filepath.Dir(fh.URI().Filename()),
121 tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
123 return &modTidyData{err: err}
125 // Keep the temporary go.mod file around long enough to parse it.
128 if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
129 return &modTidyData{err: err}
131 // Go directly to disk to get the temporary mod file, since it is
133 tempContents, err := ioutil.ReadFile(tmpURI.Filename())
135 return &modTidyData{err: err}
137 ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
139 // We do not need to worry about the temporary file's parse errors
140 // since it has been "tidied".
141 return &modTidyData{err: err}
143 // Compare the original and tidied go.mod files to compute errors and
145 errors, err := modTidyErrors(ctx, snapshot, pm, ideal, workspacePkgs)
147 return &modTidyData{err: err}
150 tidied: &source.TidiedModule{
153 TidiedContent: tempContents,
158 mth := &modTidyHandle{handle: h}
160 s.modTidyHandles[fh.URI()] = mth
163 return mth.tidy(ctx, s)
166 func (s *snapshot) parseModErrors(ctx context.Context, fh source.FileHandle, err error) (*source.TidiedModule, bool) {
171 // Match on common error messages. This is really hacky, but I'm not sure
172 // of any better way. This can be removed when golang/go#39164 is resolved.
173 case strings.Contains(err.Error(), "inconsistent vendoring"):
174 pmf, err := s.ParseMod(ctx, fh)
178 if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
181 rng, err := rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
185 args, err := source.MarshalArgs(protocol.URIFromSpanURI(fh.URI()))
189 return &source.TidiedModule{
191 Errors: []source.Error{{
194 Kind: source.ListError,
195 Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
196 See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
197 SuggestedFixes: []source.SuggestedFix{{
198 Command: &protocol.Command{
199 Command: source.CommandVendor.ID(),
200 Title: source.CommandVendor.Title,
210 func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
211 results := make(map[string]bool)
213 for _, pkg := range wsPackages {
214 for _, path := range pkg.Imports() {
215 imp := path.PkgPath()
216 if _, ok := results[imp]; !ok {
218 imports = append(imports, imp)
222 sort.Strings(imports)
223 hashed := strings.Join(imports, ",")
224 return hashContents([]byte(hashed)), nil
227 // modTidyErrors computes the differences between the original and tidied
228 // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
229 // may appear on the Go files that import packages from missing modules.
230 func modTidyErrors(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []source.Package) (errors []source.Error, err error) {
231 // First, determine which modules are unused and which are missing from the
232 // original go.mod file.
234 unused = make(map[string]*modfile.Require, len(pm.File.Require))
235 missing = make(map[string]*modfile.Require, len(ideal.Require))
236 wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
238 for _, req := range pm.File.Require {
239 unused[req.Mod.Path] = req
241 for _, req := range ideal.Require {
242 origReq := unused[req.Mod.Path]
244 missing[req.Mod.Path] = req
246 } else if origReq.Indirect != req.Indirect {
247 wrongDirectness[req.Mod.Path] = origReq
249 delete(unused, req.Mod.Path)
251 for _, req := range unused {
252 srcErr, err := unusedError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
256 errors = append(errors, srcErr)
258 for _, req := range wrongDirectness {
259 // Handle dependencies that are incorrectly labeled indirect and
261 srcErr, err := directnessError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
265 errors = append(errors, srcErr)
267 // Next, compute any diagnostics for modules that are missing from the
268 // go.mod file. The fixes will be for the go.mod file, but the
269 // diagnostics should also appear in both the go.mod file and the import
270 // statements in the Go files in which the dependencies are used.
271 missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
272 for _, req := range missing {
273 srcErr, err := missingModuleError(snapshot, pm, req)
277 missingModuleFixes[req] = srcErr.SuggestedFixes
278 errors = append(errors, srcErr)
280 // Add diagnostics for missing modules anywhere they are imported in the
282 for _, pkg := range workspacePkgs {
283 missingImports := map[string]*modfile.Require{}
284 for _, imp := range pkg.Imports() {
285 if req, ok := missing[imp.PkgPath()]; ok {
286 missingImports[imp.PkgPath()] = req
289 // If the import is a package of the dependency, then add the
290 // package to the map, this will eliminate the need to do this
291 // prefix package search on each import for each file.
295 // "golang.org/x/tools/go/expect"
296 // "golang.org/x/tools/go/packages"
298 // They both are related to the same module: "golang.org/x/tools".
300 for _, req := range ideal.Require {
301 if strings.HasPrefix(imp.PkgPath(), req.Mod.Path) && len(req.Mod.Path) > len(match) {
305 if req, ok := missing[match]; ok {
306 missingImports[imp.PkgPath()] = req
309 // None of this package's imports are from missing modules.
310 if len(missingImports) == 0 {
313 for _, pgf := range pkg.CompiledGoFiles() {
314 file, m := pgf.File, pgf.Mapper
315 if file == nil || m == nil {
318 imports := make(map[string]*ast.ImportSpec)
319 for _, imp := range file.Imports {
323 if target, err := strconv.Unquote(imp.Path.Value); err == nil {
324 imports[target] = imp
327 if len(imports) == 0 {
330 for importPath, req := range missingImports {
331 imp, ok := imports[importPath]
335 fixes, ok := missingModuleFixes[req]
337 return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
339 srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes)
343 errors = append(errors, srcErr)
350 // unusedError returns a source.Error for an unused require.
351 func unusedError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (source.Error, error) {
352 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
354 return source.Error{}, err
356 args, err := source.MarshalArgs(m.URI, false, []string{req.Mod.Path + "@none"})
358 return source.Error{}, err
361 Category: source.GoModTidy,
362 Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path),
365 SuggestedFixes: []source.SuggestedFix{{
366 Title: fmt.Sprintf("Remove dependency: %s", req.Mod.Path),
367 Command: &protocol.Command{
368 Title: source.CommandRemoveDependency.Title,
369 Command: source.CommandRemoveDependency.ID(),
376 // directnessError extracts errors when a dependency is labeled indirect when
377 // it should be direct and vice versa.
378 func directnessError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (source.Error, error) {
379 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
381 return source.Error{}, err
383 direction := "indirect"
387 // If the dependency should be direct, just highlight the // indirect.
388 if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
389 end := comments.Suffix[0].Start
390 end.LineRune += len(comments.Suffix[0].Token)
391 end.Byte += len([]byte(comments.Suffix[0].Token))
392 rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
394 return source.Error{}, err
398 // If the dependency should be indirect, add the // indirect.
399 edits, err := switchDirectness(req, m, computeEdits)
401 return source.Error{}, err
404 Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
407 Category: source.GoModTidy,
408 SuggestedFixes: []source.SuggestedFix{{
409 Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
410 Edits: map[span.URI][]protocol.TextEdit{
417 func missingModuleError(snapshot source.Snapshot, pm *source.ParsedModule, req *modfile.Require) (source.Error, error) {
418 var rng protocol.Range
419 // Default to the start of the file if there is no module declaration.
420 if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
421 start, end := pm.File.Module.Syntax.Span()
423 rng, err = rangeFromPositions(pm.Mapper, start, end)
425 return source.Error{}, err
428 args, err := source.MarshalArgs(pm.Mapper.URI, !req.Indirect, []string{req.Mod.Path + "@" + req.Mod.Version})
430 return source.Error{}, err
435 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
436 Category: source.GoModTidy,
437 Kind: source.ModTidyError,
438 SuggestedFixes: []source.SuggestedFix{{
439 Title: fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path),
440 Command: &protocol.Command{
441 Title: source.CommandAddDependency.Title,
442 Command: source.CommandAddDependency.ID(),
449 // switchDirectness gets the edits needed to change an indirect dependency to
450 // direct and vice versa.
451 func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
452 // We need a private copy of the parsed go.mod file, since we're going to
454 copied, err := modfile.Parse("", m.Content, nil)
458 // Change the directness in the matching require statement. To avoid
459 // reordering the require statements, rewrite all of them.
460 var requires []*modfile.Require
461 for _, r := range copied.Require {
462 if r.Mod.Path == req.Mod.Path {
463 requires = append(requires, &modfile.Require{
466 Indirect: !r.Indirect,
470 requires = append(requires, r)
472 copied.SetRequire(requires)
473 newContent, err := copied.Format()
477 // Calculate the edits to be made due to the change.
478 diff := computeEdits(m.URI, string(m.Content), string(newContent))
479 return source.ToProtocolEdits(m, diff)
482 // missingModuleForImport creates an error for a given import path that comes
483 // from a missing module.
484 func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (source.Error, error) {
485 if req.Syntax == nil {
486 return source.Error{}, fmt.Errorf("no syntax for %v", req)
488 spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
490 return source.Error{}, err
492 rng, err := m.Range(spn)
494 return source.Error{}, err
497 Category: source.GoModTidy,
500 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
501 Kind: source.ModTidyError,
502 SuggestedFixes: fixes,
506 func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
507 toPoint := func(offset int) (span.Point, error) {
508 l, c, err := m.Converter.ToPosition(offset)
510 return span.Point{}, err
512 return span.NewPoint(l, c, offset), nil
514 start, err := toPoint(s.Byte)
516 return protocol.Range{}, err
518 end, err := toPoint(e.Byte)
520 return protocol.Range{}, err
522 return m.Range(span.New(m.URI, start, end))