github.com/jd-ly/tools@v0.5.7/internal/lsp/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 "fmt" 10 "go/ast" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "sort" 15 "strconv" 16 "strings" 17 18 "golang.org/x/mod/modfile" 19 "github.com/jd-ly/tools/internal/event" 20 "github.com/jd-ly/tools/internal/gocommand" 21 "github.com/jd-ly/tools/internal/lsp/debug/tag" 22 "github.com/jd-ly/tools/internal/lsp/diff" 23 "github.com/jd-ly/tools/internal/lsp/protocol" 24 "github.com/jd-ly/tools/internal/lsp/source" 25 "github.com/jd-ly/tools/internal/memoize" 26 "github.com/jd-ly/tools/internal/span" 27 ) 28 29 type modTidyKey struct { 30 sessionID string 31 env string 32 gomod source.FileIdentity 33 imports string 34 unsavedOverlays string 35 view string 36 } 37 38 type modTidyHandle struct { 39 handle *memoize.Handle 40 } 41 42 type modTidyData struct { 43 tidied *source.TidiedModule 44 err error 45 } 46 47 func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) { 48 v, err := mth.handle.Get(ctx, snapshot.generation, snapshot) 49 if err != nil { 50 return nil, err 51 } 52 data := v.(*modTidyData) 53 return data.tidied, data.err 54 } 55 56 func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { 57 if pm.File == nil { 58 return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI) 59 } 60 if handle := s.getModTidyHandle(pm.URI); handle != nil { 61 return handle.tidy(ctx, s) 62 } 63 fh, err := s.GetFile(ctx, pm.URI) 64 if err != nil { 65 return nil, err 66 } 67 // If the file handle is an overlay, it may not be written to disk. 68 // The go.mod file has to be on disk for `go mod tidy` to work. 69 if _, ok := fh.(*overlay); ok { 70 if info, _ := os.Stat(fh.URI().Filename()); info == nil { 71 return nil, source.ErrNoModOnDisk 72 } 73 } 74 workspacePkgs, err := s.WorkspacePackages(ctx) 75 if err != nil { 76 if tm, ok := s.parseModErrors(ctx, fh, err); ok { 77 return tm, nil 78 } 79 return nil, err 80 } 81 importHash, err := hashImports(ctx, workspacePkgs) 82 if err != nil { 83 return nil, err 84 } 85 86 s.mu.Lock() 87 overlayHash := hashUnsavedOverlays(s.files) 88 s.mu.Unlock() 89 90 key := modTidyKey{ 91 sessionID: s.view.session.id, 92 view: s.view.folder.Filename(), 93 imports: importHash, 94 unsavedOverlays: overlayHash, 95 gomod: fh.FileIdentity(), 96 env: hashEnv(s), 97 } 98 h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { 99 ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI())) 100 defer done() 101 102 snapshot := arg.(*snapshot) 103 inv := &gocommand.Invocation{ 104 Verb: "mod", 105 Args: []string{"tidy"}, 106 WorkingDir: filepath.Dir(fh.URI().Filename()), 107 } 108 tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile|source.AllowNetwork, inv) 109 if err != nil { 110 return &modTidyData{err: err} 111 } 112 // Keep the temporary go.mod file around long enough to parse it. 113 defer cleanup() 114 115 if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil { 116 return &modTidyData{err: err} 117 } 118 // Go directly to disk to get the temporary mod file, since it is 119 // always on disk. 120 tempContents, err := ioutil.ReadFile(tmpURI.Filename()) 121 if err != nil { 122 return &modTidyData{err: err} 123 } 124 ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil) 125 if err != nil { 126 // We do not need to worry about the temporary file's parse errors 127 // since it has been "tidied". 128 return &modTidyData{err: err} 129 } 130 // Compare the original and tidied go.mod files to compute errors and 131 // suggested fixes. 132 errors, err := modTidyErrors(ctx, snapshot, pm, ideal, workspacePkgs) 133 if err != nil { 134 return &modTidyData{err: err} 135 } 136 return &modTidyData{ 137 tidied: &source.TidiedModule{ 138 Errors: errors, 139 TidiedContent: tempContents, 140 }, 141 } 142 }, nil) 143 144 mth := &modTidyHandle{handle: h} 145 s.mu.Lock() 146 s.modTidyHandles[fh.URI()] = mth 147 s.mu.Unlock() 148 149 return mth.tidy(ctx, s) 150 } 151 152 func (s *snapshot) parseModErrors(ctx context.Context, fh source.FileHandle, goCommandErr error) (*source.TidiedModule, bool) { 153 if goCommandErr == nil { 154 return nil, false 155 } 156 157 // Match on common error messages. This is really hacky, but I'm not sure 158 // of any better way. This can be removed when golang/go#39164 is resolved. 159 errText := goCommandErr.Error() 160 isInconsistentVendor := strings.Contains(errText, "inconsistent vendoring") 161 isGoSumUpdates := strings.Contains(errText, "updates to go.sum needed") || strings.Contains(errText, "missing go.sum entry") 162 163 if !isInconsistentVendor && !isGoSumUpdates { 164 return nil, false 165 } 166 167 pmf, err := s.ParseMod(ctx, fh) 168 if err != nil { 169 return nil, false 170 } 171 if pmf.File.Module == nil || pmf.File.Module.Syntax == nil { 172 return nil, false 173 } 174 rng, err := rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End) 175 if err != nil { 176 return nil, false 177 } 178 args, err := source.MarshalArgs(protocol.URIFromSpanURI(fh.URI())) 179 if err != nil { 180 return nil, false 181 } 182 183 switch { 184 case isInconsistentVendor: 185 return &source.TidiedModule{ 186 Errors: []*source.Error{{ 187 URI: fh.URI(), 188 Range: rng, 189 Kind: source.ListError, 190 Message: `Inconsistent vendoring detected. Please re-run "go mod vendor". 191 See https://github.com/golang/go/issues/39164 for more detail on this issue.`, 192 SuggestedFixes: []source.SuggestedFix{{ 193 Title: source.CommandVendor.Title, 194 Command: &protocol.Command{ 195 Command: source.CommandVendor.ID(), 196 Title: source.CommandVendor.Title, 197 Arguments: args, 198 }, 199 }}, 200 }}, 201 }, true 202 203 case isGoSumUpdates: 204 return &source.TidiedModule{ 205 Errors: []*source.Error{{ 206 URI: fh.URI(), 207 Range: rng, 208 Kind: source.ListError, 209 Message: `go.sum is out of sync with go.mod. Please update it or run "go mod tidy".`, 210 SuggestedFixes: []source.SuggestedFix{ 211 { 212 Command: &protocol.Command{ 213 Command: source.CommandTidy.ID(), 214 Title: source.CommandTidy.Title, 215 Arguments: args, 216 }, 217 }, 218 { 219 Command: &protocol.Command{ 220 Command: source.CommandUpdateGoSum.ID(), 221 Title: source.CommandUpdateGoSum.Title, 222 Arguments: args, 223 }, 224 }, 225 }, 226 }}, 227 }, true 228 } 229 230 return nil, false 231 } 232 233 func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) { 234 results := make(map[string]bool) 235 var imports []string 236 for _, pkg := range wsPackages { 237 for _, path := range pkg.Imports() { 238 imp := path.PkgPath() 239 if _, ok := results[imp]; !ok { 240 results[imp] = true 241 imports = append(imports, imp) 242 } 243 } 244 imports = append(imports, pkg.MissingDependencies()...) 245 } 246 sort.Strings(imports) 247 hashed := strings.Join(imports, ",") 248 return hashContents([]byte(hashed)), nil 249 } 250 251 // modTidyErrors computes the differences between the original and tidied 252 // go.mod files to produce diagnostic and suggested fixes. Some diagnostics 253 // may appear on the Go files that import packages from missing modules. 254 func modTidyErrors(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []source.Package) (errors []*source.Error, err error) { 255 // First, determine which modules are unused and which are missing from the 256 // original go.mod file. 257 var ( 258 unused = make(map[string]*modfile.Require, len(pm.File.Require)) 259 missing = make(map[string]*modfile.Require, len(ideal.Require)) 260 wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require)) 261 ) 262 for _, req := range pm.File.Require { 263 unused[req.Mod.Path] = req 264 } 265 for _, req := range ideal.Require { 266 origReq := unused[req.Mod.Path] 267 if origReq == nil { 268 missing[req.Mod.Path] = req 269 continue 270 } else if origReq.Indirect != req.Indirect { 271 wrongDirectness[req.Mod.Path] = origReq 272 } 273 delete(unused, req.Mod.Path) 274 } 275 for _, req := range unused { 276 srcErr, err := unusedError(pm.Mapper, req, snapshot.View().Options().ComputeEdits) 277 if err != nil { 278 return nil, err 279 } 280 errors = append(errors, srcErr) 281 } 282 for _, req := range wrongDirectness { 283 // Handle dependencies that are incorrectly labeled indirect and 284 // vice versa. 285 srcErr, err := directnessError(pm.Mapper, req, snapshot.View().Options().ComputeEdits) 286 if err != nil { 287 return nil, err 288 } 289 errors = append(errors, srcErr) 290 } 291 // Next, compute any diagnostics for modules that are missing from the 292 // go.mod file. The fixes will be for the go.mod file, but the 293 // diagnostics should also appear in both the go.mod file and the import 294 // statements in the Go files in which the dependencies are used. 295 missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{} 296 for _, req := range missing { 297 srcErr, err := missingModuleError(snapshot, pm, req) 298 if err != nil { 299 return nil, err 300 } 301 missingModuleFixes[req] = srcErr.SuggestedFixes 302 errors = append(errors, srcErr) 303 } 304 // Add diagnostics for missing modules anywhere they are imported in the 305 // workspace. 306 for _, pkg := range workspacePkgs { 307 missingImports := map[string]*modfile.Require{} 308 var importedPkgs []string 309 310 // If -mod=readonly is not set we may have successfully imported 311 // packages from missing modules. Otherwise they'll be in 312 // MissingDependencies. Combine both. 313 for _, imp := range pkg.Imports() { 314 importedPkgs = append(importedPkgs, imp.PkgPath()) 315 } 316 importedPkgs = append(importedPkgs, pkg.MissingDependencies()...) 317 318 for _, imp := range importedPkgs { 319 if req, ok := missing[imp]; ok { 320 missingImports[imp] = req 321 break 322 } 323 // If the import is a package of the dependency, then add the 324 // package to the map, this will eliminate the need to do this 325 // prefix package search on each import for each file. 326 // Example: 327 // 328 // import ( 329 // "github.com/jd-ly/tools/go/expect" 330 // "github.com/jd-ly/tools/go/packages" 331 // ) 332 // They both are related to the same module: "github.com/jd-ly/tools". 333 var match string 334 for _, req := range ideal.Require { 335 if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) { 336 match = req.Mod.Path 337 } 338 } 339 if req, ok := missing[match]; ok { 340 missingImports[imp] = req 341 } 342 } 343 // None of this package's imports are from missing modules. 344 if len(missingImports) == 0 { 345 continue 346 } 347 for _, pgf := range pkg.CompiledGoFiles() { 348 file, m := pgf.File, pgf.Mapper 349 if file == nil || m == nil { 350 continue 351 } 352 imports := make(map[string]*ast.ImportSpec) 353 for _, imp := range file.Imports { 354 if imp.Path == nil { 355 continue 356 } 357 if target, err := strconv.Unquote(imp.Path.Value); err == nil { 358 imports[target] = imp 359 } 360 } 361 if len(imports) == 0 { 362 continue 363 } 364 for importPath, req := range missingImports { 365 imp, ok := imports[importPath] 366 if !ok { 367 continue 368 } 369 fixes, ok := missingModuleFixes[req] 370 if !ok { 371 return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path) 372 } 373 srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes) 374 if err != nil { 375 return nil, err 376 } 377 errors = append(errors, srcErr) 378 } 379 } 380 } 381 return errors, nil 382 } 383 384 // unusedError returns a source.Error for an unused require. 385 func unusedError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Error, error) { 386 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) 387 if err != nil { 388 return nil, err 389 } 390 args, err := source.MarshalArgs(m.URI, false, []string{req.Mod.Path + "@none"}) 391 if err != nil { 392 return nil, err 393 } 394 return &source.Error{ 395 Category: source.GoModTidy, 396 Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path), 397 Range: rng, 398 URI: m.URI, 399 SuggestedFixes: []source.SuggestedFix{{ 400 Title: fmt.Sprintf("Remove dependency: %s", req.Mod.Path), 401 Command: &protocol.Command{ 402 Title: source.CommandRemoveDependency.Title, 403 Command: source.CommandRemoveDependency.ID(), 404 Arguments: args, 405 }, 406 }}, 407 }, nil 408 } 409 410 // directnessError extracts errors when a dependency is labeled indirect when 411 // it should be direct and vice versa. 412 func directnessError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Error, error) { 413 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) 414 if err != nil { 415 return nil, err 416 } 417 direction := "indirect" 418 if req.Indirect { 419 direction = "direct" 420 421 // If the dependency should be direct, just highlight the // indirect. 422 if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 { 423 end := comments.Suffix[0].Start 424 end.LineRune += len(comments.Suffix[0].Token) 425 end.Byte += len([]byte(comments.Suffix[0].Token)) 426 rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end) 427 if err != nil { 428 return nil, err 429 } 430 } 431 } 432 // If the dependency should be indirect, add the // indirect. 433 edits, err := switchDirectness(req, m, computeEdits) 434 if err != nil { 435 return nil, err 436 } 437 return &source.Error{ 438 Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction), 439 Range: rng, 440 URI: m.URI, 441 Category: source.GoModTidy, 442 SuggestedFixes: []source.SuggestedFix{{ 443 Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction), 444 Edits: map[span.URI][]protocol.TextEdit{ 445 m.URI: edits, 446 }, 447 }}, 448 }, nil 449 } 450 451 func missingModuleError(snapshot source.Snapshot, pm *source.ParsedModule, req *modfile.Require) (*source.Error, error) { 452 var rng protocol.Range 453 // Default to the start of the file if there is no module declaration. 454 if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil { 455 start, end := pm.File.Module.Syntax.Span() 456 var err error 457 rng, err = rangeFromPositions(pm.Mapper, start, end) 458 if err != nil { 459 return nil, err 460 } 461 } 462 args, err := source.MarshalArgs(pm.Mapper.URI, !req.Indirect, []string{req.Mod.Path + "@" + req.Mod.Version}) 463 if err != nil { 464 return nil, err 465 } 466 return &source.Error{ 467 URI: pm.Mapper.URI, 468 Range: rng, 469 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), 470 Category: source.GoModTidy, 471 Kind: source.ModTidyError, 472 SuggestedFixes: []source.SuggestedFix{{ 473 Title: fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path), 474 Command: &protocol.Command{ 475 Title: source.CommandAddDependency.Title, 476 Command: source.CommandAddDependency.ID(), 477 Arguments: args, 478 }, 479 }}, 480 }, nil 481 } 482 483 // switchDirectness gets the edits needed to change an indirect dependency to 484 // direct and vice versa. 485 func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) { 486 // We need a private copy of the parsed go.mod file, since we're going to 487 // modify it. 488 copied, err := modfile.Parse("", m.Content, nil) 489 if err != nil { 490 return nil, err 491 } 492 // Change the directness in the matching require statement. To avoid 493 // reordering the require statements, rewrite all of them. 494 var requires []*modfile.Require 495 for _, r := range copied.Require { 496 if r.Mod.Path == req.Mod.Path { 497 requires = append(requires, &modfile.Require{ 498 Mod: r.Mod, 499 Syntax: r.Syntax, 500 Indirect: !r.Indirect, 501 }) 502 continue 503 } 504 requires = append(requires, r) 505 } 506 copied.SetRequire(requires) 507 newContent, err := copied.Format() 508 if err != nil { 509 return nil, err 510 } 511 // Calculate the edits to be made due to the change. 512 diff := computeEdits(m.URI, string(m.Content), string(newContent)) 513 return source.ToProtocolEdits(m, diff) 514 } 515 516 // missingModuleForImport creates an error for a given import path that comes 517 // from a missing module. 518 func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Error, error) { 519 if req.Syntax == nil { 520 return nil, fmt.Errorf("no syntax for %v", req) 521 } 522 spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span() 523 if err != nil { 524 return nil, err 525 } 526 rng, err := m.Range(spn) 527 if err != nil { 528 return nil, err 529 } 530 return &source.Error{ 531 Category: source.GoModTidy, 532 URI: m.URI, 533 Range: rng, 534 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), 535 Kind: source.ModTidyError, 536 SuggestedFixes: fixes, 537 }, nil 538 } 539 540 func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) { 541 toPoint := func(offset int) (span.Point, error) { 542 l, c, err := m.Converter.ToPosition(offset) 543 if err != nil { 544 return span.Point{}, err 545 } 546 return span.NewPoint(l, c, offset), nil 547 } 548 start, err := toPoint(s.Byte) 549 if err != nil { 550 return protocol.Range{}, err 551 } 552 end, err := toPoint(e.Byte) 553 if err != nil { 554 return protocol.Range{}, err 555 } 556 return m.Range(span.New(m.URI, start, end)) 557 }