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/command"
22 "golang.org/x/tools/internal/lsp/debug/tag"
23 "golang.org/x/tools/internal/lsp/diff"
24 "golang.org/x/tools/internal/lsp/protocol"
25 "golang.org/x/tools/internal/lsp/source"
26 "golang.org/x/tools/internal/memoize"
27 "golang.org/x/tools/internal/span"
30 type modTidyKey struct {
33 gomod source.FileIdentity
35 unsavedOverlays string
39 type modTidyHandle struct {
40 handle *memoize.Handle
43 type modTidyData struct {
44 tidied *source.TidiedModule
48 func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) {
49 v, err := mth.handle.Get(ctx, snapshot.generation, snapshot)
53 data := v.(*modTidyData)
54 return data.tidied, data.err
57 func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
59 return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI)
61 if handle := s.getModTidyHandle(pm.URI); handle != nil {
62 return handle.tidy(ctx, s)
64 fh, err := s.GetFile(ctx, pm.URI)
68 // If the file handle is an overlay, it may not be written to disk.
69 // The go.mod file has to be on disk for `go mod tidy` to work.
70 if _, ok := fh.(*overlay); ok {
71 if info, _ := os.Stat(fh.URI().Filename()); info == nil {
72 return nil, source.ErrNoModOnDisk
75 if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
76 return &source.TidiedModule{
77 Diagnostics: criticalErr.DiagList,
80 workspacePkgs, err := s.WorkspacePackages(ctx)
84 importHash, err := hashImports(ctx, workspacePkgs)
90 overlayHash := hashUnsavedOverlays(s.files)
94 sessionID: s.view.session.id,
95 view: s.view.folder.Filename(),
97 unsavedOverlays: overlayHash,
98 gomod: fh.FileIdentity(),
101 h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
102 ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
105 snapshot := arg.(*snapshot)
106 inv := &gocommand.Invocation{
108 Args: []string{"tidy"},
109 WorkingDir: filepath.Dir(fh.URI().Filename()),
111 tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
113 return &modTidyData{err: err}
115 // Keep the temporary go.mod file around long enough to parse it.
118 if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
119 return &modTidyData{err: err}
121 // Go directly to disk to get the temporary mod file, since it is
123 tempContents, err := ioutil.ReadFile(tmpURI.Filename())
125 return &modTidyData{err: err}
127 ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
129 // We do not need to worry about the temporary file's parse errors
130 // since it has been "tidied".
131 return &modTidyData{err: err}
133 // Compare the original and tidied go.mod files to compute errors and
135 diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs)
137 return &modTidyData{err: err}
140 tidied: &source.TidiedModule{
141 Diagnostics: diagnostics,
142 TidiedContent: tempContents,
147 mth := &modTidyHandle{handle: h}
149 s.modTidyHandles[fh.URI()] = mth
152 return mth.tidy(ctx, s)
155 func (s *snapshot) uriToModDecl(ctx context.Context, uri span.URI) (protocol.Range, error) {
156 fh, err := s.GetFile(ctx, uri)
158 return protocol.Range{}, nil
160 pmf, err := s.ParseMod(ctx, fh)
162 return protocol.Range{}, nil
164 if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
165 return protocol.Range{}, nil
167 return rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
170 func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
171 results := make(map[string]bool)
173 for _, pkg := range wsPackages {
174 for _, path := range pkg.Imports() {
175 imp := path.PkgPath()
176 if _, ok := results[imp]; !ok {
178 imports = append(imports, imp)
181 imports = append(imports, pkg.MissingDependencies()...)
183 sort.Strings(imports)
184 hashed := strings.Join(imports, ",")
185 return hashContents([]byte(hashed)), nil
188 // modTidyDiagnostics computes the differences between the original and tidied
189 // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
190 // may appear on the Go files that import packages from missing modules.
191 func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []source.Package) (diagnostics []*source.Diagnostic, err error) {
192 // First, determine which modules are unused and which are missing from the
193 // original go.mod file.
195 unused = make(map[string]*modfile.Require, len(pm.File.Require))
196 missing = make(map[string]*modfile.Require, len(ideal.Require))
197 wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
199 for _, req := range pm.File.Require {
200 unused[req.Mod.Path] = req
202 for _, req := range ideal.Require {
203 origReq := unused[req.Mod.Path]
205 missing[req.Mod.Path] = req
207 } else if origReq.Indirect != req.Indirect {
208 wrongDirectness[req.Mod.Path] = origReq
210 delete(unused, req.Mod.Path)
212 for _, req := range wrongDirectness {
213 // Handle dependencies that are incorrectly labeled indirect and
215 srcDiag, err := directnessDiagnostic(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
219 diagnostics = append(diagnostics, srcDiag)
221 // Next, compute any diagnostics for modules that are missing from the
222 // go.mod file. The fixes will be for the go.mod file, but the
223 // diagnostics should also appear in both the go.mod file and the import
224 // statements in the Go files in which the dependencies are used.
225 missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
226 for _, req := range missing {
227 srcDiag, err := missingModuleDiagnostic(snapshot, pm, req)
231 missingModuleFixes[req] = srcDiag.SuggestedFixes
232 diagnostics = append(diagnostics, srcDiag)
234 // Add diagnostics for missing modules anywhere they are imported in the
236 for _, pkg := range workspacePkgs {
237 missingImports := map[string]*modfile.Require{}
238 var importedPkgs []string
240 // If -mod=readonly is not set we may have successfully imported
241 // packages from missing modules. Otherwise they'll be in
242 // MissingDependencies. Combine both.
243 for _, imp := range pkg.Imports() {
244 importedPkgs = append(importedPkgs, imp.PkgPath())
246 importedPkgs = append(importedPkgs, pkg.MissingDependencies()...)
248 for _, imp := range importedPkgs {
249 if req, ok := missing[imp]; ok {
250 missingImports[imp] = req
253 // If the import is a package of the dependency, then add the
254 // package to the map, this will eliminate the need to do this
255 // prefix package search on each import for each file.
259 // "golang.org/x/tools/go/expect"
260 // "golang.org/x/tools/go/packages"
262 // They both are related to the same module: "golang.org/x/tools".
264 for _, req := range ideal.Require {
265 if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
269 if req, ok := missing[match]; ok {
270 missingImports[imp] = req
273 // None of this package's imports are from missing modules.
274 if len(missingImports) == 0 {
277 for _, pgf := range pkg.CompiledGoFiles() {
278 file, m := pgf.File, pgf.Mapper
279 if file == nil || m == nil {
282 imports := make(map[string]*ast.ImportSpec)
283 for _, imp := range file.Imports {
287 if target, err := strconv.Unquote(imp.Path.Value); err == nil {
288 imports[target] = imp
291 if len(imports) == 0 {
294 for importPath, req := range missingImports {
295 imp, ok := imports[importPath]
299 fixes, ok := missingModuleFixes[req]
301 return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
303 srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes)
307 diagnostics = append(diagnostics, srcErr)
311 // Finally, add errors for any unused dependencies.
312 onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
313 for _, req := range unused {
314 srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic, snapshot.View().Options().ComputeEdits)
318 diagnostics = append(diagnostics, srcErr)
320 return diagnostics, nil
323 // unusedDiagnostic returns a source.Diagnostic for an unused require.
324 func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool, computeEdits diff.ComputeEdits) (*source.Diagnostic, error) {
325 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
329 title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path)
330 cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{
331 URI: protocol.URIFromSpanURI(m.URI),
332 OnlyDiagnostic: onlyDiagnostic,
333 ModulePath: req.Mod.Path,
338 return &source.Diagnostic{
341 Severity: protocol.SeverityWarning,
342 Source: source.ModTidyError,
343 Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path),
344 SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
348 // directnessDiagnostic extracts errors when a dependency is labeled indirect when
349 // it should be direct and vice versa.
350 func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Diagnostic, error) {
351 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
355 direction := "indirect"
359 // If the dependency should be direct, just highlight the // indirect.
360 if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
361 end := comments.Suffix[0].Start
362 end.LineRune += len(comments.Suffix[0].Token)
363 end.Byte += len([]byte(comments.Suffix[0].Token))
364 rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
370 // If the dependency should be indirect, add the // indirect.
371 edits, err := switchDirectness(req, m, computeEdits)
375 return &source.Diagnostic{
378 Severity: protocol.SeverityWarning,
379 Source: source.ModTidyError,
380 Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
381 SuggestedFixes: []source.SuggestedFix{{
382 Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
383 Edits: map[span.URI][]protocol.TextEdit{
390 func missingModuleDiagnostic(snapshot source.Snapshot, pm *source.ParsedModule, req *modfile.Require) (*source.Diagnostic, error) {
391 var rng protocol.Range
392 // Default to the start of the file if there is no module declaration.
393 if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
394 start, end := pm.File.Module.Syntax.Span()
396 rng, err = rangeFromPositions(pm.Mapper, start, end)
401 title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path)
402 cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
403 URI: protocol.URIFromSpanURI(pm.Mapper.URI),
404 AddRequire: !req.Indirect,
405 GoCmdArgs: []string{req.Mod.Path + "@" + req.Mod.Version},
410 return &source.Diagnostic{
413 Severity: protocol.SeverityError,
414 Source: source.ModTidyError,
415 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
416 SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
420 // switchDirectness gets the edits needed to change an indirect dependency to
421 // direct and vice versa.
422 func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
423 // We need a private copy of the parsed go.mod file, since we're going to
425 copied, err := modfile.Parse("", m.Content, nil)
429 // Change the directness in the matching require statement. To avoid
430 // reordering the require statements, rewrite all of them.
431 var requires []*modfile.Require
432 for _, r := range copied.Require {
433 if r.Mod.Path == req.Mod.Path {
434 requires = append(requires, &modfile.Require{
437 Indirect: !r.Indirect,
441 requires = append(requires, r)
443 copied.SetRequire(requires)
444 newContent, err := copied.Format()
448 // Calculate the edits to be made due to the change.
449 diff, err := computeEdits(m.URI, string(m.Content), string(newContent))
453 return source.ToProtocolEdits(m, diff)
456 // missingModuleForImport creates an error for a given import path that comes
457 // from a missing module.
458 func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) {
459 if req.Syntax == nil {
460 return nil, fmt.Errorf("no syntax for %v", req)
462 spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
466 rng, err := m.Range(spn)
470 return &source.Diagnostic{
473 Severity: protocol.SeverityError,
474 Source: source.ModTidyError,
475 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
476 SuggestedFixes: fixes,
480 func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
481 spn, err := spanFromPositions(m, s, e)
483 return protocol.Range{}, err
488 func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) {
489 toPoint := func(offset int) (span.Point, error) {
490 l, c, err := m.Converter.ToPosition(offset)
492 return span.Point{}, err
494 return span.NewPoint(l, c, offset), nil
496 start, err := toPoint(s.Byte)
498 return span.Span{}, err
500 end, err := toPoint(e.Byte)
502 return span.Span{}, err
504 return span.New(m.URI, start, end), nil