blob: fa30df18e367d791067565380abccd6395a5052c [file] [log] [blame]
Robert Findleyb15dac22022-08-30 14:40:12 -04001// 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.
4
5package cache
6
7import (
8 "context"
9 "fmt"
10 "go/ast"
11 "go/token"
12 "io/ioutil"
13 "os"
14 "path/filepath"
15 "strconv"
16 "strings"
17
18 "golang.org/x/mod/modfile"
Robert Findleyb15dac22022-08-30 14:40:12 -040019 "golang.org/x/tools/gopls/internal/lsp/command"
Robert Findleyb15dac22022-08-30 14:40:12 -040020 "golang.org/x/tools/gopls/internal/lsp/protocol"
21 "golang.org/x/tools/gopls/internal/lsp/source"
Alan Donovan26a95e62022-10-07 10:40:32 -040022 "golang.org/x/tools/gopls/internal/span"
Alan Donovan98560772022-09-30 14:18:37 -040023 "golang.org/x/tools/internal/event"
24 "golang.org/x/tools/internal/event/tag"
25 "golang.org/x/tools/internal/gocommand"
Robert Findleyb15dac22022-08-30 14:40:12 -040026 "golang.org/x/tools/internal/memoize"
Robert Findleyb15dac22022-08-30 14:40:12 -040027)
28
29// ModTidy returns the go.mod file that would be obtained by running
30// "go mod tidy". Concurrent requests are combined into a single command.
31func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
32 uri := pm.URI
33 if pm.File == nil {
34 return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri)
35 }
36
37 s.mu.Lock()
38 entry, hit := s.modTidyHandles.Get(uri)
39 s.mu.Unlock()
40
41 type modTidyResult struct {
42 tidied *source.TidiedModule
43 err error
44 }
45
46 // Cache miss?
47 if !hit {
48 // If the file handle is an overlay, it may not be written to disk.
49 // The go.mod file has to be on disk for `go mod tidy` to work.
50 // TODO(rfindley): is this still true with Go 1.16 overlay support?
51 fh, err := s.GetFile(ctx, pm.URI)
52 if err != nil {
53 return nil, err
54 }
55 if _, ok := fh.(*overlay); ok {
56 if info, _ := os.Stat(uri.Filename()); info == nil {
57 return nil, source.ErrNoModOnDisk
58 }
59 }
60
61 if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
62 return &source.TidiedModule{
63 Diagnostics: criticalErr.Diagnostics,
64 }, nil
65 }
Robert Findleybf5db812022-12-02 17:02:23 -050066 if ctx.Err() != nil { // must check ctx after GetCriticalError
67 return nil, ctx.Err()
68 }
Robert Findleyb15dac22022-08-30 14:40:12 -040069
70 if err := s.awaitLoaded(ctx); err != nil {
71 return nil, err
72 }
73
74 handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} {
75 tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm)
76 return modTidyResult{tidied, err}
77 })
78
79 entry = handle
80 s.mu.Lock()
81 s.modTidyHandles.Set(uri, entry, nil)
82 s.mu.Unlock()
83 }
84
85 // Await result.
86 v, err := s.awaitPromise(ctx, entry.(*memoize.Promise))
87 if err != nil {
88 return nil, err
89 }
90 res := v.(modTidyResult)
91 return res.tidied, res.err
92}
93
94// modTidyImpl runs "go mod tidy" on a go.mod file.
95func modTidyImpl(ctx context.Context, snapshot *snapshot, filename string, pm *source.ParsedModule) (*source.TidiedModule, error) {
96 ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename))
97 defer done()
98
99 inv := &gocommand.Invocation{
100 Verb: "mod",
101 Args: []string{"tidy"},
102 WorkingDir: filepath.Dir(filename),
103 }
104 // TODO(adonovan): ensure that unsaved overlays are passed through to 'go'.
105 tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
106 if err != nil {
107 return nil, err
108 }
109 // Keep the temporary go.mod file around long enough to parse it.
110 defer cleanup()
111
Robert Findleye8a70a52022-11-28 14:58:06 -0500112 if _, err := snapshot.view.gocmdRunner.Run(ctx, *inv); err != nil {
Robert Findleyb15dac22022-08-30 14:40:12 -0400113 return nil, err
114 }
115
116 // Go directly to disk to get the temporary mod file,
117 // since it is always on disk.
118 tempContents, err := ioutil.ReadFile(tmpURI.Filename())
119 if err != nil {
120 return nil, err
121 }
122 ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
123 if err != nil {
124 // We do not need to worry about the temporary file's parse errors
125 // since it has been "tidied".
126 return nil, err
127 }
128
129 // Compare the original and tidied go.mod files to compute errors and
130 // suggested fixes.
131 diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal)
132 if err != nil {
133 return nil, err
134 }
135
136 return &source.TidiedModule{
137 Diagnostics: diagnostics,
138 TidiedContent: tempContents,
139 }, nil
140}
141
142// modTidyDiagnostics computes the differences between the original and tidied
143// go.mod files to produce diagnostic and suggested fixes. Some diagnostics
144// may appear on the Go files that import packages from missing modules.
145func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File) (diagnostics []*source.Diagnostic, err error) {
146 // First, determine which modules are unused and which are missing from the
147 // original go.mod file.
148 var (
149 unused = make(map[string]*modfile.Require, len(pm.File.Require))
150 missing = make(map[string]*modfile.Require, len(ideal.Require))
151 wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
152 )
153 for _, req := range pm.File.Require {
154 unused[req.Mod.Path] = req
155 }
156 for _, req := range ideal.Require {
157 origReq := unused[req.Mod.Path]
158 if origReq == nil {
159 missing[req.Mod.Path] = req
160 continue
161 } else if origReq.Indirect != req.Indirect {
162 wrongDirectness[req.Mod.Path] = origReq
163 }
164 delete(unused, req.Mod.Path)
165 }
166 for _, req := range wrongDirectness {
167 // Handle dependencies that are incorrectly labeled indirect and
168 // vice versa.
169 srcDiag, err := directnessDiagnostic(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
170 if err != nil {
171 // We're probably in a bad state if we can't compute a
172 // directnessDiagnostic, but try to keep going so as to not suppress
173 // other, valid diagnostics.
174 event.Error(ctx, "computing directness diagnostic", err)
175 continue
176 }
177 diagnostics = append(diagnostics, srcDiag)
178 }
179 // Next, compute any diagnostics for modules that are missing from the
180 // go.mod file. The fixes will be for the go.mod file, but the
181 // diagnostics should also appear in both the go.mod file and the import
182 // statements in the Go files in which the dependencies are used.
183 missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
184 for _, req := range missing {
185 srcDiag, err := missingModuleDiagnostic(pm, req)
186 if err != nil {
187 return nil, err
188 }
189 missingModuleFixes[req] = srcDiag.SuggestedFixes
190 diagnostics = append(diagnostics, srcDiag)
191 }
192 // Add diagnostics for missing modules anywhere they are imported in the
193 // workspace.
194 // TODO(adonovan): opt: opportunities for parallelism abound.
Alan Donovan18f76ec2022-12-08 09:48:21 -0500195 for _, m := range snapshot.workspaceMetadata() {
Robert Findleyb15dac22022-08-30 14:40:12 -0400196 // Read both lists of files of this package, in parallel.
Robert Findley5a4eba52022-11-03 18:28:39 -0400197 goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m)
Robert Findleyb15dac22022-08-30 14:40:12 -0400198 if err != nil {
199 return nil, err
200 }
201
202 missingImports := map[string]*modfile.Require{}
203
204 // If -mod=readonly is not set we may have successfully imported
205 // packages from missing modules. Otherwise they'll be in
206 // MissingDependencies. Combine both.
207 for imp := range parseImports(ctx, snapshot, goFiles) {
208 if req, ok := missing[imp]; ok {
209 missingImports[imp] = req
210 break
211 }
212 // If the import is a package of the dependency, then add the
213 // package to the map, this will eliminate the need to do this
214 // prefix package search on each import for each file.
215 // Example:
216 //
217 // import (
218 // "golang.org/x/tools/go/expect"
219 // "golang.org/x/tools/go/packages"
220 // )
221 // They both are related to the same module: "golang.org/x/tools".
222 var match string
223 for _, req := range ideal.Require {
224 if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
225 match = req.Mod.Path
226 }
227 }
228 if req, ok := missing[match]; ok {
229 missingImports[imp] = req
230 }
231 }
232 // None of this package's imports are from missing modules.
233 if len(missingImports) == 0 {
234 continue
235 }
236 for _, goFile := range compiledGoFiles {
237 pgf, err := snapshot.ParseGo(ctx, goFile, source.ParseHeader)
238 if err != nil {
239 continue
240 }
241 file, m := pgf.File, pgf.Mapper
242 if file == nil || m == nil {
243 continue
244 }
245 imports := make(map[string]*ast.ImportSpec)
246 for _, imp := range file.Imports {
247 if imp.Path == nil {
248 continue
249 }
250 if target, err := strconv.Unquote(imp.Path.Value); err == nil {
251 imports[target] = imp
252 }
253 }
254 if len(imports) == 0 {
255 continue
256 }
257 for importPath, req := range missingImports {
258 imp, ok := imports[importPath]
259 if !ok {
260 continue
261 }
262 fixes, ok := missingModuleFixes[req]
263 if !ok {
264 return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
265 }
266 srcErr, err := missingModuleForImport(pgf.Tok, m, imp, req, fixes)
267 if err != nil {
268 return nil, err
269 }
270 diagnostics = append(diagnostics, srcErr)
271 }
272 }
273 }
274 // Finally, add errors for any unused dependencies.
275 onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
276 for _, req := range unused {
277 srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
278 if err != nil {
279 return nil, err
280 }
281 diagnostics = append(diagnostics, srcErr)
282 }
283 return diagnostics, nil
284}
285
286// unusedDiagnostic returns a source.Diagnostic for an unused require.
287func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) {
Alan Donovan3beecff2022-10-10 22:49:16 -0400288 rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
Robert Findleyb15dac22022-08-30 14:40:12 -0400289 if err != nil {
290 return nil, err
291 }
292 title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path)
293 cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{
294 URI: protocol.URIFromSpanURI(m.URI),
295 OnlyDiagnostic: onlyDiagnostic,
296 ModulePath: req.Mod.Path,
297 })
298 if err != nil {
299 return nil, err
300 }
301 return &source.Diagnostic{
302 URI: m.URI,
303 Range: rng,
304 Severity: protocol.SeverityWarning,
305 Source: source.ModTidyError,
306 Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path),
307 SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
308 }, nil
309}
310
311// directnessDiagnostic extracts errors when a dependency is labeled indirect when
312// it should be direct and vice versa.
Alan Donovan98560772022-09-30 14:18:37 -0400313func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) {
Alan Donovan3beecff2022-10-10 22:49:16 -0400314 rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
Robert Findleyb15dac22022-08-30 14:40:12 -0400315 if err != nil {
316 return nil, err
317 }
318 direction := "indirect"
319 if req.Indirect {
320 direction = "direct"
321
322 // If the dependency should be direct, just highlight the // indirect.
323 if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
324 end := comments.Suffix[0].Start
325 end.LineRune += len(comments.Suffix[0].Token)
Alan Donovan3beecff2022-10-10 22:49:16 -0400326 end.Byte += len(comments.Suffix[0].Token)
327 rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte)
Robert Findleyb15dac22022-08-30 14:40:12 -0400328 if err != nil {
329 return nil, err
330 }
331 }
332 }
333 // If the dependency should be indirect, add the // indirect.
334 edits, err := switchDirectness(req, m, computeEdits)
335 if err != nil {
336 return nil, err
337 }
338 return &source.Diagnostic{
339 URI: m.URI,
340 Range: rng,
341 Severity: protocol.SeverityWarning,
342 Source: source.ModTidyError,
343 Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
344 SuggestedFixes: []source.SuggestedFix{{
345 Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
346 Edits: map[span.URI][]protocol.TextEdit{
347 m.URI: edits,
348 },
349 ActionKind: protocol.QuickFix,
350 }},
351 }, nil
352}
353
354func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*source.Diagnostic, error) {
355 var rng protocol.Range
356 // Default to the start of the file if there is no module declaration.
357 if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
358 start, end := pm.File.Module.Syntax.Span()
359 var err error
Alan Donovan3beecff2022-10-10 22:49:16 -0400360 rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte)
Robert Findleyb15dac22022-08-30 14:40:12 -0400361 if err != nil {
362 return nil, err
363 }
364 }
365 title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path)
366 cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
367 URI: protocol.URIFromSpanURI(pm.Mapper.URI),
368 AddRequire: !req.Indirect,
369 GoCmdArgs: []string{req.Mod.Path + "@" + req.Mod.Version},
370 })
371 if err != nil {
372 return nil, err
373 }
374 return &source.Diagnostic{
375 URI: pm.Mapper.URI,
376 Range: rng,
377 Severity: protocol.SeverityError,
378 Source: source.ModTidyError,
379 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
380 SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
381 }, nil
382}
383
384// switchDirectness gets the edits needed to change an indirect dependency to
385// direct and vice versa.
Alan Donovan98560772022-09-30 14:18:37 -0400386func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits source.DiffFunction) ([]protocol.TextEdit, error) {
Robert Findleyb15dac22022-08-30 14:40:12 -0400387 // We need a private copy of the parsed go.mod file, since we're going to
388 // modify it.
389 copied, err := modfile.Parse("", m.Content, nil)
390 if err != nil {
391 return nil, err
392 }
393 // Change the directness in the matching require statement. To avoid
394 // reordering the require statements, rewrite all of them.
395 var requires []*modfile.Require
396 seenVersions := make(map[string]string)
397 for _, r := range copied.Require {
398 if seen := seenVersions[r.Mod.Path]; seen != "" && seen != r.Mod.Version {
399 // Avoid a panic in SetRequire below, which panics on conflicting
400 // versions.
401 return nil, fmt.Errorf("%q has conflicting versions: %q and %q", r.Mod.Path, seen, r.Mod.Version)
402 }
403 seenVersions[r.Mod.Path] = r.Mod.Version
404 if r.Mod.Path == req.Mod.Path {
405 requires = append(requires, &modfile.Require{
406 Mod: r.Mod,
407 Syntax: r.Syntax,
408 Indirect: !r.Indirect,
409 })
410 continue
411 }
412 requires = append(requires, r)
413 }
414 copied.SetRequire(requires)
415 newContent, err := copied.Format()
416 if err != nil {
417 return nil, err
418 }
419 // Calculate the edits to be made due to the change.
Alan Donovand96b2382022-09-30 21:58:21 -0400420 edits := computeEdits(string(m.Content), string(newContent))
Alan Donovan98560772022-09-30 14:18:37 -0400421 return source.ToProtocolEdits(m, edits)
Robert Findleyb15dac22022-08-30 14:40:12 -0400422}
423
424// missingModuleForImport creates an error for a given import path that comes
425// from a missing module.
426func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) {
427 if req.Syntax == nil {
428 return nil, fmt.Errorf("no syntax for %v", req)
429 }
Alan Donovan3beecff2022-10-10 22:49:16 -0400430 rng, err := m.PosRange(imp.Path.Pos(), imp.Path.End())
Robert Findleyb15dac22022-08-30 14:40:12 -0400431 if err != nil {
432 return nil, err
433 }
434 return &source.Diagnostic{
435 URI: m.URI,
436 Range: rng,
437 Severity: protocol.SeverityError,
438 Source: source.ModTidyError,
439 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
440 SuggestedFixes: fixes,
441 }, nil
442}
443
Robert Findleyb15dac22022-08-30 14:40:12 -0400444func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) {
445 toPoint := func(offset int) (span.Point, error) {
446 l, c, err := span.ToPosition(m.TokFile, offset)
447 if err != nil {
448 return span.Point{}, err
449 }
450 return span.NewPoint(l, c, offset), nil
451 }
452 start, err := toPoint(s.Byte)
453 if err != nil {
454 return span.Span{}, err
455 }
456 end, err := toPoint(e.Byte)
457 if err != nil {
458 return span.Span{}, err
459 }
460 return span.New(m.URI, start, end), nil
461}
462
463// parseImports parses the headers of the specified files and returns
464// the set of strings that appear in import declarations within
465// GoFiles. Errors are ignored.
466//
Alan Donovan91311ab2022-10-18 10:28:21 -0400467// (We can't simply use Metadata.Imports because it is based on
468// CompiledGoFiles, after cgo processing.)
Robert Findleyb15dac22022-08-30 14:40:12 -0400469func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) map[string]bool {
470 s.mu.Lock() // peekOrParse requires a locked snapshot (!)
471 defer s.mu.Unlock()
472 seen := make(map[string]bool)
473 for _, file := range files {
474 f, err := peekOrParse(ctx, s, file, source.ParseHeader)
475 if err != nil {
476 continue
477 }
478 for _, spec := range f.File.Imports {
479 path, _ := strconv.Unquote(spec.Path.Value)
480 seen[path] = true
481 }
482 }
483 return seen
484}