golang.org/x/tools/gopls@v0.15.3/internal/cache/mod.go (about) 1 // Copyright 2019 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 "path/filepath" 12 "regexp" 13 "strings" 14 15 "golang.org/x/mod/modfile" 16 "golang.org/x/mod/module" 17 "golang.org/x/tools/gopls/internal/file" 18 "golang.org/x/tools/gopls/internal/protocol" 19 "golang.org/x/tools/gopls/internal/protocol/command" 20 "golang.org/x/tools/internal/event" 21 "golang.org/x/tools/internal/event/tag" 22 "golang.org/x/tools/internal/gocommand" 23 "golang.org/x/tools/internal/memoize" 24 ) 25 26 // A ParsedModule contains the results of parsing a go.mod file. 27 type ParsedModule struct { 28 URI protocol.DocumentURI 29 File *modfile.File 30 Mapper *protocol.Mapper 31 ParseErrors []*Diagnostic 32 } 33 34 // ParseMod parses a go.mod file, using a cache. It may return partial results and an error. 35 func (s *Snapshot) ParseMod(ctx context.Context, fh file.Handle) (*ParsedModule, error) { 36 uri := fh.URI() 37 38 s.mu.Lock() 39 entry, hit := s.parseModHandles.Get(uri) 40 s.mu.Unlock() 41 42 type parseModKey file.Identity 43 type parseModResult struct { 44 parsed *ParsedModule 45 err error 46 } 47 48 // cache miss? 49 if !hit { 50 promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} { 51 parsed, err := parseModImpl(ctx, fh) 52 return parseModResult{parsed, err} 53 }) 54 55 entry = promise 56 s.mu.Lock() 57 s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() }) 58 s.mu.Unlock() 59 } 60 61 // Await result. 62 v, err := s.awaitPromise(ctx, entry) 63 if err != nil { 64 return nil, err 65 } 66 res := v.(parseModResult) 67 return res.parsed, res.err 68 } 69 70 // parseModImpl parses the go.mod file whose name and contents are in fh. 71 // It may return partial results and an error. 72 func parseModImpl(ctx context.Context, fh file.Handle) (*ParsedModule, error) { 73 _, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI())) 74 defer done() 75 76 contents, err := fh.Content() 77 if err != nil { 78 return nil, err 79 } 80 m := protocol.NewMapper(fh.URI(), contents) 81 file, parseErr := modfile.Parse(fh.URI().Path(), contents, nil) 82 // Attempt to convert the error to a standardized parse error. 83 var parseErrors []*Diagnostic 84 if parseErr != nil { 85 mfErrList, ok := parseErr.(modfile.ErrorList) 86 if !ok { 87 return nil, fmt.Errorf("unexpected parse error type %v", parseErr) 88 } 89 for _, mfErr := range mfErrList { 90 rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) 91 if err != nil { 92 return nil, err 93 } 94 parseErrors = append(parseErrors, &Diagnostic{ 95 URI: fh.URI(), 96 Range: rng, 97 Severity: protocol.SeverityError, 98 Source: ParseError, 99 Message: mfErr.Err.Error(), 100 }) 101 } 102 } 103 return &ParsedModule{ 104 URI: fh.URI(), 105 Mapper: m, 106 File: file, 107 ParseErrors: parseErrors, 108 }, parseErr 109 } 110 111 // A ParsedWorkFile contains the results of parsing a go.work file. 112 type ParsedWorkFile struct { 113 URI protocol.DocumentURI 114 File *modfile.WorkFile 115 Mapper *protocol.Mapper 116 ParseErrors []*Diagnostic 117 } 118 119 // ParseWork parses a go.work file, using a cache. It may return partial results and an error. 120 // TODO(adonovan): move to new work.go file. 121 func (s *Snapshot) ParseWork(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) { 122 uri := fh.URI() 123 124 s.mu.Lock() 125 entry, hit := s.parseWorkHandles.Get(uri) 126 s.mu.Unlock() 127 128 type parseWorkKey file.Identity 129 type parseWorkResult struct { 130 parsed *ParsedWorkFile 131 err error 132 } 133 134 // cache miss? 135 if !hit { 136 handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} { 137 parsed, err := parseWorkImpl(ctx, fh) 138 return parseWorkResult{parsed, err} 139 }) 140 141 entry = handle 142 s.mu.Lock() 143 s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() }) 144 s.mu.Unlock() 145 } 146 147 // Await result. 148 v, err := s.awaitPromise(ctx, entry) 149 if err != nil { 150 return nil, err 151 } 152 res := v.(parseWorkResult) 153 return res.parsed, res.err 154 } 155 156 // parseWorkImpl parses a go.work file. It may return partial results and an error. 157 func parseWorkImpl(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) { 158 _, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI())) 159 defer done() 160 161 content, err := fh.Content() 162 if err != nil { 163 return nil, err 164 } 165 m := protocol.NewMapper(fh.URI(), content) 166 file, parseErr := modfile.ParseWork(fh.URI().Path(), content, nil) 167 // Attempt to convert the error to a standardized parse error. 168 var parseErrors []*Diagnostic 169 if parseErr != nil { 170 mfErrList, ok := parseErr.(modfile.ErrorList) 171 if !ok { 172 return nil, fmt.Errorf("unexpected parse error type %v", parseErr) 173 } 174 for _, mfErr := range mfErrList { 175 rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) 176 if err != nil { 177 return nil, err 178 } 179 parseErrors = append(parseErrors, &Diagnostic{ 180 URI: fh.URI(), 181 Range: rng, 182 Severity: protocol.SeverityError, 183 Source: ParseError, 184 Message: mfErr.Err.Error(), 185 }) 186 } 187 } 188 return &ParsedWorkFile{ 189 URI: fh.URI(), 190 Mapper: m, 191 File: file, 192 ParseErrors: parseErrors, 193 }, parseErr 194 } 195 196 // goSum reads the go.sum file for the go.mod file at modURI, if it exists. If 197 // it doesn't exist, it returns nil. 198 func (s *Snapshot) goSum(ctx context.Context, modURI protocol.DocumentURI) []byte { 199 // Get the go.sum file, either from the snapshot or directly from the 200 // cache. Avoid (*snapshot).ReadFile here, as we don't want to add 201 // nonexistent file handles to the snapshot if the file does not exist. 202 // 203 // TODO(rfindley): but that's not right. Changes to sum files should 204 // invalidate content, even if it's nonexistent content. 205 sumURI := protocol.URIFromPath(sumFilename(modURI)) 206 sumFH := s.FindFile(sumURI) 207 if sumFH == nil { 208 var err error 209 sumFH, err = s.view.fs.ReadFile(ctx, sumURI) 210 if err != nil { 211 return nil 212 } 213 } 214 content, err := sumFH.Content() 215 if err != nil { 216 return nil 217 } 218 return content 219 } 220 221 func sumFilename(modURI protocol.DocumentURI) string { 222 return strings.TrimSuffix(modURI.Path(), ".mod") + ".sum" 223 } 224 225 // ModWhy returns the "go mod why" result for each module named in a 226 // require statement in the go.mod file. 227 // TODO(adonovan): move to new mod_why.go file. 228 func (s *Snapshot) ModWhy(ctx context.Context, fh file.Handle) (map[string]string, error) { 229 uri := fh.URI() 230 231 if s.FileKind(fh) != file.Mod { 232 return nil, fmt.Errorf("%s is not a go.mod file", uri) 233 } 234 235 s.mu.Lock() 236 entry, hit := s.modWhyHandles.Get(uri) 237 s.mu.Unlock() 238 239 type modWhyResult struct { 240 why map[string]string 241 err error 242 } 243 244 // cache miss? 245 if !hit { 246 handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} { 247 why, err := modWhyImpl(ctx, arg.(*Snapshot), fh) 248 return modWhyResult{why, err} 249 }) 250 251 entry = handle 252 s.mu.Lock() 253 s.modWhyHandles.Set(uri, entry, nil) 254 s.mu.Unlock() 255 } 256 257 // Await result. 258 v, err := s.awaitPromise(ctx, entry) 259 if err != nil { 260 return nil, err 261 } 262 res := v.(modWhyResult) 263 return res.why, res.err 264 } 265 266 // modWhyImpl returns the result of "go mod why -m" on the specified go.mod file. 267 func modWhyImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) (map[string]string, error) { 268 ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI())) 269 defer done() 270 271 pm, err := snapshot.ParseMod(ctx, fh) 272 if err != nil { 273 return nil, err 274 } 275 // No requires to explain. 276 if len(pm.File.Require) == 0 { 277 return nil, nil // empty result 278 } 279 // Run `go mod why` on all the dependencies. 280 inv := &gocommand.Invocation{ 281 Verb: "mod", 282 Args: []string{"why", "-m"}, 283 WorkingDir: filepath.Dir(fh.URI().Path()), 284 } 285 for _, req := range pm.File.Require { 286 inv.Args = append(inv.Args, req.Mod.Path) 287 } 288 stdout, err := snapshot.RunGoCommandDirect(ctx, Normal, inv) 289 if err != nil { 290 return nil, err 291 } 292 whyList := strings.Split(stdout.String(), "\n\n") 293 if len(whyList) != len(pm.File.Require) { 294 return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)) 295 } 296 why := make(map[string]string, len(pm.File.Require)) 297 for i, req := range pm.File.Require { 298 why[req.Mod.Path] = whyList[i] 299 } 300 return why, nil 301 } 302 303 // extractGoCommandErrors tries to parse errors that come from the go command 304 // and shape them into go.mod diagnostics. 305 // TODO: rename this to 'load errors' 306 func (s *Snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) []*Diagnostic { 307 if goCmdError == nil { 308 return nil 309 } 310 311 type locatedErr struct { 312 loc protocol.Location 313 msg string 314 } 315 diagLocations := map[*ParsedModule]locatedErr{} 316 backupDiagLocations := map[*ParsedModule]locatedErr{} 317 318 // If moduleErrs is non-nil, go command errors are scoped to specific 319 // modules. 320 var moduleErrs *moduleErrorMap 321 _ = errors.As(goCmdError, &moduleErrs) 322 323 // Match the error against all the mod files in the workspace. 324 for _, uri := range s.View().ModFiles() { 325 fh, err := s.ReadFile(ctx, uri) 326 if err != nil { 327 event.Error(ctx, "getting modfile for Go command error", err) 328 continue 329 } 330 pm, err := s.ParseMod(ctx, fh) 331 if err != nil { 332 // Parsing errors are reported elsewhere 333 return nil 334 } 335 var msgs []string // error messages to consider 336 if moduleErrs != nil { 337 if pm.File.Module != nil { 338 for _, mes := range moduleErrs.errs[pm.File.Module.Mod.Path] { 339 msgs = append(msgs, mes.Error()) 340 } 341 } 342 } else { 343 msgs = append(msgs, goCmdError.Error()) 344 } 345 for _, msg := range msgs { 346 if strings.Contains(goCmdError.Error(), "errors parsing go.mod") { 347 // The go command emits parse errors for completely invalid go.mod files. 348 // Those are reported by our own diagnostics and can be ignored here. 349 // As of writing, we are not aware of any other errors that include 350 // file/position information, so don't even try to find it. 351 continue 352 } 353 loc, found, err := s.matchErrorToModule(pm, msg) 354 if err != nil { 355 event.Error(ctx, "matching error to module", err) 356 continue 357 } 358 le := locatedErr{ 359 loc: loc, 360 msg: msg, 361 } 362 if found { 363 diagLocations[pm] = le 364 } else { 365 backupDiagLocations[pm] = le 366 } 367 } 368 } 369 370 // If we didn't find any good matches, assign diagnostics to all go.mod files. 371 if len(diagLocations) == 0 { 372 diagLocations = backupDiagLocations 373 } 374 375 var srcErrs []*Diagnostic 376 for pm, le := range diagLocations { 377 diag, err := s.goCommandDiagnostic(pm, le.loc, le.msg) 378 if err != nil { 379 event.Error(ctx, "building go command diagnostic", err) 380 continue 381 } 382 srcErrs = append(srcErrs, diag) 383 } 384 return srcErrs 385 } 386 387 var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`) 388 389 // matchErrorToModule matches a go command error message to a go.mod file. 390 // Some examples: 391 // 392 // example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory 393 // go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72 394 // go: example.com@v1.2.3 requires\n\trandom.org@v1.2.3: parsing go.mod:\n\tmodule declares its path as: bob.org\n\tbut was required as: random.org 395 // 396 // It returns the location of a reference to the one of the modules and true 397 // if one exists. If none is found it returns a fallback location and false. 398 func (s *Snapshot) matchErrorToModule(pm *ParsedModule, goCmdError string) (protocol.Location, bool, error) { 399 var reference *modfile.Line 400 matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) 401 402 for i := len(matches) - 1; i >= 0; i-- { 403 ver := module.Version{Path: matches[i][1], Version: matches[i][2]} 404 if err := module.Check(ver.Path, ver.Version); err != nil { 405 continue 406 } 407 reference = findModuleReference(pm.File, ver) 408 if reference != nil { 409 break 410 } 411 } 412 413 if reference == nil { 414 // No match for the module path was found in the go.mod file. 415 // Show the error on the module declaration, if one exists, or 416 // just the first line of the file. 417 var start, end int 418 if pm.File.Module != nil && pm.File.Module.Syntax != nil { 419 syntax := pm.File.Module.Syntax 420 start, end = syntax.Start.Byte, syntax.End.Byte 421 } 422 loc, err := pm.Mapper.OffsetLocation(start, end) 423 return loc, false, err 424 } 425 426 loc, err := pm.Mapper.OffsetLocation(reference.Start.Byte, reference.End.Byte) 427 return loc, true, err 428 } 429 430 // goCommandDiagnostic creates a diagnostic for a given go command error. 431 func (s *Snapshot) goCommandDiagnostic(pm *ParsedModule, loc protocol.Location, goCmdError string) (*Diagnostic, error) { 432 matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) 433 var innermost *module.Version 434 for i := len(matches) - 1; i >= 0; i-- { 435 ver := module.Version{Path: matches[i][1], Version: matches[i][2]} 436 if err := module.Check(ver.Path, ver.Version); err != nil { 437 continue 438 } 439 innermost = &ver 440 break 441 } 442 443 switch { 444 case strings.Contains(goCmdError, "inconsistent vendoring"): 445 cmd, err := command.NewVendorCommand("Run go mod vendor", command.URIArg{URI: pm.URI}) 446 if err != nil { 447 return nil, err 448 } 449 return &Diagnostic{ 450 URI: pm.URI, 451 Range: loc.Range, 452 Severity: protocol.SeverityError, 453 Source: ListError, 454 Message: `Inconsistent vendoring detected. Please re-run "go mod vendor". 455 See https://github.com/golang/go/issues/39164 for more detail on this issue.`, 456 SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, 457 }, nil 458 459 case strings.Contains(goCmdError, "updates to go.sum needed"), strings.Contains(goCmdError, "missing go.sum entry"): 460 var args []protocol.DocumentURI 461 args = append(args, s.View().ModFiles()...) 462 tidyCmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args}) 463 if err != nil { 464 return nil, err 465 } 466 updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args}) 467 if err != nil { 468 return nil, err 469 } 470 msg := "go.sum is out of sync with go.mod. Please update it by applying the quick fix." 471 if innermost != nil { 472 msg = fmt.Sprintf("go.sum is out of sync with go.mod: entry for %v is missing. Please updating it by applying the quick fix.", innermost) 473 } 474 return &Diagnostic{ 475 URI: pm.URI, 476 Range: loc.Range, 477 Severity: protocol.SeverityError, 478 Source: ListError, 479 Message: msg, 480 SuggestedFixes: []SuggestedFix{ 481 SuggestedFixFromCommand(tidyCmd, protocol.QuickFix), 482 SuggestedFixFromCommand(updateCmd, protocol.QuickFix), 483 }, 484 }, nil 485 case strings.Contains(goCmdError, "disabled by GOPROXY=off") && innermost != nil: 486 title := fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version) 487 cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ 488 URI: pm.URI, 489 AddRequire: false, 490 GoCmdArgs: []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)}, 491 }) 492 if err != nil { 493 return nil, err 494 } 495 return &Diagnostic{ 496 URI: pm.URI, 497 Range: loc.Range, 498 Severity: protocol.SeverityError, 499 Message: fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version), 500 Source: ListError, 501 SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, 502 }, nil 503 default: 504 return &Diagnostic{ 505 URI: pm.URI, 506 Range: loc.Range, 507 Severity: protocol.SeverityError, 508 Source: ListError, 509 Message: goCmdError, 510 }, nil 511 } 512 } 513 514 func findModuleReference(mf *modfile.File, ver module.Version) *modfile.Line { 515 for _, req := range mf.Require { 516 if req.Mod == ver { 517 return req.Syntax 518 } 519 } 520 for _, ex := range mf.Exclude { 521 if ex.Mod == ver { 522 return ex.Syntax 523 } 524 } 525 for _, rep := range mf.Replace { 526 if rep.New == ver || rep.Old == ver { 527 return rep.Syntax 528 } 529 } 530 return nil 531 }