github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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/powerman/golang-tools/internal/event" 20 "github.com/powerman/golang-tools/internal/gocommand" 21 "github.com/powerman/golang-tools/internal/lsp/command" 22 "github.com/powerman/golang-tools/internal/lsp/debug/tag" 23 "github.com/powerman/golang-tools/internal/lsp/diff" 24 "github.com/powerman/golang-tools/internal/lsp/protocol" 25 "github.com/powerman/golang-tools/internal/lsp/source" 26 "github.com/powerman/golang-tools/internal/memoize" 27 "github.com/powerman/golang-tools/internal/span" 28 ) 29 30 type modTidyKey struct { 31 sessionID string 32 env string 33 gomod source.FileIdentity 34 imports string 35 unsavedOverlays string 36 view string 37 } 38 39 type modTidyHandle struct { 40 handle *memoize.Handle 41 } 42 43 type modTidyData struct { 44 tidied *source.TidiedModule 45 err error 46 } 47 48 func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) { 49 v, err := mth.handle.Get(ctx, snapshot.generation, snapshot) 50 if err != nil { 51 return nil, err 52 } 53 data := v.(*modTidyData) 54 return data.tidied, data.err 55 } 56 57 func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { 58 if pm.File == nil { 59 return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI) 60 } 61 if handle := s.getModTidyHandle(pm.URI); handle != nil { 62 return handle.tidy(ctx, s) 63 } 64 fh, err := s.GetFile(ctx, pm.URI) 65 if err != nil { 66 return nil, err 67 } 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 73 } 74 } 75 if criticalErr := s.GetCriticalError(ctx); criticalErr != nil { 76 return &source.TidiedModule{ 77 Diagnostics: criticalErr.DiagList, 78 }, nil 79 } 80 workspacePkgs, err := s.workspacePackageHandles(ctx) 81 if err != nil { 82 return nil, err 83 } 84 importHash, err := s.hashImports(ctx, workspacePkgs) 85 if err != nil { 86 return nil, err 87 } 88 89 s.mu.Lock() 90 overlayHash := hashUnsavedOverlays(s.files) 91 s.mu.Unlock() 92 93 key := modTidyKey{ 94 sessionID: s.view.session.id, 95 view: s.view.folder.Filename(), 96 imports: importHash, 97 unsavedOverlays: overlayHash, 98 gomod: fh.FileIdentity(), 99 env: hashEnv(s), 100 } 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())) 103 defer done() 104 105 snapshot := arg.(*snapshot) 106 inv := &gocommand.Invocation{ 107 Verb: "mod", 108 Args: []string{"tidy"}, 109 WorkingDir: filepath.Dir(fh.URI().Filename()), 110 } 111 tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv) 112 if err != nil { 113 return &modTidyData{err: err} 114 } 115 // Keep the temporary go.mod file around long enough to parse it. 116 defer cleanup() 117 118 if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil { 119 return &modTidyData{err: err} 120 } 121 // Go directly to disk to get the temporary mod file, since it is 122 // always on disk. 123 tempContents, err := ioutil.ReadFile(tmpURI.Filename()) 124 if err != nil { 125 return &modTidyData{err: err} 126 } 127 ideal, err := modfile.Parse(tmpURI.Filename(), 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 &modTidyData{err: err} 132 } 133 // Compare the original and tidied go.mod files to compute errors and 134 // suggested fixes. 135 diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs) 136 if err != nil { 137 return &modTidyData{err: err} 138 } 139 return &modTidyData{ 140 tidied: &source.TidiedModule{ 141 Diagnostics: diagnostics, 142 TidiedContent: tempContents, 143 }, 144 } 145 }, nil) 146 147 mth := &modTidyHandle{handle: h} 148 s.mu.Lock() 149 s.modTidyHandles[fh.URI()] = mth 150 s.mu.Unlock() 151 152 return mth.tidy(ctx, s) 153 } 154 155 func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) (string, error) { 156 seen := map[string]struct{}{} 157 var imports []string 158 for _, ph := range wsPackages { 159 for _, imp := range ph.imports(ctx, s) { 160 if _, ok := seen[imp]; !ok { 161 imports = append(imports, imp) 162 seen[imp] = struct{}{} 163 } 164 } 165 } 166 sort.Strings(imports) 167 hashed := strings.Join(imports, ",") 168 return hashContents([]byte(hashed)), nil 169 } 170 171 // modTidyDiagnostics computes the differences between the original and tidied 172 // go.mod files to produce diagnostic and suggested fixes. Some diagnostics 173 // may appear on the Go files that import packages from missing modules. 174 func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []*packageHandle) (diagnostics []*source.Diagnostic, err error) { 175 // First, determine which modules are unused and which are missing from the 176 // original go.mod file. 177 var ( 178 unused = make(map[string]*modfile.Require, len(pm.File.Require)) 179 missing = make(map[string]*modfile.Require, len(ideal.Require)) 180 wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require)) 181 ) 182 for _, req := range pm.File.Require { 183 unused[req.Mod.Path] = req 184 } 185 for _, req := range ideal.Require { 186 origReq := unused[req.Mod.Path] 187 if origReq == nil { 188 missing[req.Mod.Path] = req 189 continue 190 } else if origReq.Indirect != req.Indirect { 191 wrongDirectness[req.Mod.Path] = origReq 192 } 193 delete(unused, req.Mod.Path) 194 } 195 for _, req := range wrongDirectness { 196 // Handle dependencies that are incorrectly labeled indirect and 197 // vice versa. 198 srcDiag, err := directnessDiagnostic(pm.Mapper, req, snapshot.View().Options().ComputeEdits) 199 if err != nil { 200 // We're probably in a bad state if we can't compute a 201 // directnessDiagnostic, but try to keep going so as to not suppress 202 // other, valid diagnostics. 203 event.Error(ctx, "computing directness diagnostic", err) 204 continue 205 } 206 diagnostics = append(diagnostics, srcDiag) 207 } 208 // Next, compute any diagnostics for modules that are missing from the 209 // go.mod file. The fixes will be for the go.mod file, but the 210 // diagnostics should also appear in both the go.mod file and the import 211 // statements in the Go files in which the dependencies are used. 212 missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{} 213 for _, req := range missing { 214 srcDiag, err := missingModuleDiagnostic(pm, req) 215 if err != nil { 216 return nil, err 217 } 218 missingModuleFixes[req] = srcDiag.SuggestedFixes 219 diagnostics = append(diagnostics, srcDiag) 220 } 221 // Add diagnostics for missing modules anywhere they are imported in the 222 // workspace. 223 for _, ph := range workspacePkgs { 224 missingImports := map[string]*modfile.Require{} 225 226 // If -mod=readonly is not set we may have successfully imported 227 // packages from missing modules. Otherwise they'll be in 228 // MissingDependencies. Combine both. 229 importedPkgs := ph.imports(ctx, snapshot) 230 231 for _, imp := range importedPkgs { 232 if req, ok := missing[imp]; ok { 233 missingImports[imp] = req 234 break 235 } 236 // If the import is a package of the dependency, then add the 237 // package to the map, this will eliminate the need to do this 238 // prefix package search on each import for each file. 239 // Example: 240 // 241 // import ( 242 // "github.com/powerman/golang-tools/go/expect" 243 // "github.com/powerman/golang-tools/go/packages" 244 // ) 245 // They both are related to the same module: "github.com/powerman/golang-tools". 246 var match string 247 for _, req := range ideal.Require { 248 if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) { 249 match = req.Mod.Path 250 } 251 } 252 if req, ok := missing[match]; ok { 253 missingImports[imp] = req 254 } 255 } 256 // None of this package's imports are from missing modules. 257 if len(missingImports) == 0 { 258 continue 259 } 260 for _, pgh := range ph.compiledGoFiles { 261 pgf, err := snapshot.ParseGo(ctx, pgh.file, source.ParseHeader) 262 if err != nil { 263 continue 264 } 265 file, m := pgf.File, pgf.Mapper 266 if file == nil || m == nil { 267 continue 268 } 269 imports := make(map[string]*ast.ImportSpec) 270 for _, imp := range file.Imports { 271 if imp.Path == nil { 272 continue 273 } 274 if target, err := strconv.Unquote(imp.Path.Value); err == nil { 275 imports[target] = imp 276 } 277 } 278 if len(imports) == 0 { 279 continue 280 } 281 for importPath, req := range missingImports { 282 imp, ok := imports[importPath] 283 if !ok { 284 continue 285 } 286 fixes, ok := missingModuleFixes[req] 287 if !ok { 288 return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path) 289 } 290 srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes) 291 if err != nil { 292 return nil, err 293 } 294 diagnostics = append(diagnostics, srcErr) 295 } 296 } 297 } 298 // Finally, add errors for any unused dependencies. 299 onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1 300 for _, req := range unused { 301 srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic) 302 if err != nil { 303 return nil, err 304 } 305 diagnostics = append(diagnostics, srcErr) 306 } 307 return diagnostics, nil 308 } 309 310 // unusedDiagnostic returns a source.Diagnostic for an unused require. 311 func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) { 312 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) 313 if err != nil { 314 return nil, err 315 } 316 title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path) 317 cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{ 318 URI: protocol.URIFromSpanURI(m.URI), 319 OnlyDiagnostic: onlyDiagnostic, 320 ModulePath: req.Mod.Path, 321 }) 322 if err != nil { 323 return nil, err 324 } 325 return &source.Diagnostic{ 326 URI: m.URI, 327 Range: rng, 328 Severity: protocol.SeverityWarning, 329 Source: source.ModTidyError, 330 Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path), 331 SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, 332 }, nil 333 } 334 335 // directnessDiagnostic extracts errors when a dependency is labeled indirect when 336 // it should be direct and vice versa. 337 func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Diagnostic, error) { 338 rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) 339 if err != nil { 340 return nil, err 341 } 342 direction := "indirect" 343 if req.Indirect { 344 direction = "direct" 345 346 // If the dependency should be direct, just highlight the // indirect. 347 if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 { 348 end := comments.Suffix[0].Start 349 end.LineRune += len(comments.Suffix[0].Token) 350 end.Byte += len([]byte(comments.Suffix[0].Token)) 351 rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end) 352 if err != nil { 353 return nil, err 354 } 355 } 356 } 357 // If the dependency should be indirect, add the // indirect. 358 edits, err := switchDirectness(req, m, computeEdits) 359 if err != nil { 360 return nil, err 361 } 362 return &source.Diagnostic{ 363 URI: m.URI, 364 Range: rng, 365 Severity: protocol.SeverityWarning, 366 Source: source.ModTidyError, 367 Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction), 368 SuggestedFixes: []source.SuggestedFix{{ 369 Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction), 370 Edits: map[span.URI][]protocol.TextEdit{ 371 m.URI: edits, 372 }, 373 ActionKind: protocol.QuickFix, 374 }}, 375 }, nil 376 } 377 378 func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*source.Diagnostic, error) { 379 var rng protocol.Range 380 // Default to the start of the file if there is no module declaration. 381 if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil { 382 start, end := pm.File.Module.Syntax.Span() 383 var err error 384 rng, err = rangeFromPositions(pm.Mapper, start, end) 385 if err != nil { 386 return nil, err 387 } 388 } 389 title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path) 390 cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ 391 URI: protocol.URIFromSpanURI(pm.Mapper.URI), 392 AddRequire: !req.Indirect, 393 GoCmdArgs: []string{req.Mod.Path + "@" + req.Mod.Version}, 394 }) 395 if err != nil { 396 return nil, err 397 } 398 return &source.Diagnostic{ 399 URI: pm.Mapper.URI, 400 Range: rng, 401 Severity: protocol.SeverityError, 402 Source: source.ModTidyError, 403 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), 404 SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, 405 }, nil 406 } 407 408 // switchDirectness gets the edits needed to change an indirect dependency to 409 // direct and vice versa. 410 func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) { 411 // We need a private copy of the parsed go.mod file, since we're going to 412 // modify it. 413 copied, err := modfile.Parse("", m.Content, nil) 414 if err != nil { 415 return nil, err 416 } 417 // Change the directness in the matching require statement. To avoid 418 // reordering the require statements, rewrite all of them. 419 var requires []*modfile.Require 420 seenVersions := make(map[string]string) 421 for _, r := range copied.Require { 422 if seen := seenVersions[r.Mod.Path]; seen != "" && seen != r.Mod.Version { 423 // Avoid a panic in SetRequire below, which panics on conflicting 424 // versions. 425 return nil, fmt.Errorf("%q has conflicting versions: %q and %q", r.Mod.Path, seen, r.Mod.Version) 426 } 427 seenVersions[r.Mod.Path] = r.Mod.Version 428 if r.Mod.Path == req.Mod.Path { 429 requires = append(requires, &modfile.Require{ 430 Mod: r.Mod, 431 Syntax: r.Syntax, 432 Indirect: !r.Indirect, 433 }) 434 continue 435 } 436 requires = append(requires, r) 437 } 438 copied.SetRequire(requires) 439 newContent, err := copied.Format() 440 if err != nil { 441 return nil, err 442 } 443 // Calculate the edits to be made due to the change. 444 diff, err := computeEdits(m.URI, string(m.Content), string(newContent)) 445 if err != nil { 446 return nil, err 447 } 448 return source.ToProtocolEdits(m, diff) 449 } 450 451 // missingModuleForImport creates an error for a given import path that comes 452 // from a missing module. 453 func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { 454 if req.Syntax == nil { 455 return nil, fmt.Errorf("no syntax for %v", req) 456 } 457 spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span() 458 if err != nil { 459 return nil, err 460 } 461 rng, err := m.Range(spn) 462 if err != nil { 463 return nil, err 464 } 465 return &source.Diagnostic{ 466 URI: m.URI, 467 Range: rng, 468 Severity: protocol.SeverityError, 469 Source: source.ModTidyError, 470 Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), 471 SuggestedFixes: fixes, 472 }, nil 473 } 474 475 func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) { 476 spn, err := spanFromPositions(m, s, e) 477 if err != nil { 478 return protocol.Range{}, err 479 } 480 return m.Range(spn) 481 } 482 483 func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) { 484 toPoint := func(offset int) (span.Point, error) { 485 l, c, err := m.Converter.ToPosition(offset) 486 if err != nil { 487 return span.Point{}, err 488 } 489 return span.NewPoint(l, c, offset), nil 490 } 491 start, err := toPoint(s.Byte) 492 if err != nil { 493 return span.Span{}, err 494 } 495 end, err := toPoint(e.Byte) 496 if err != nil { 497 return span.Span{}, err 498 } 499 return span.New(m.URI, start, end), nil 500 }