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