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