github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/cache/workspace.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 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 "sync" 15 16 "golang.org/x/mod/modfile" 17 "github.com/powerman/golang-tools/internal/event" 18 "github.com/powerman/golang-tools/internal/lsp/source" 19 "github.com/powerman/golang-tools/internal/span" 20 "github.com/powerman/golang-tools/internal/xcontext" 21 errors "golang.org/x/xerrors" 22 ) 23 24 // workspaceSource reports how the set of active modules has been derived. 25 type workspaceSource int 26 27 const ( 28 legacyWorkspace = iota // non-module or single module mode 29 goplsModWorkspace // modules provided by a gopls.mod file 30 goWorkWorkspace // modules provided by a go.work file 31 fileSystemWorkspace // modules scanned from the filesystem 32 ) 33 34 func (s workspaceSource) String() string { 35 switch s { 36 case legacyWorkspace: 37 return "legacy" 38 case goplsModWorkspace: 39 return "gopls.mod" 40 case goWorkWorkspace: 41 return "go.work" 42 case fileSystemWorkspace: 43 return "file system" 44 default: 45 return "!(unknown module source)" 46 } 47 } 48 49 // workspace tracks go.mod files in the workspace, along with the 50 // gopls.mod file, to provide support for multi-module workspaces. 51 // 52 // Specifically, it provides: 53 // - the set of modules contained within in the workspace root considered to 54 // be 'active' 55 // - the workspace modfile, to be used for the go command `-modfile` flag 56 // - the set of workspace directories 57 // 58 // This type is immutable (or rather, idempotent), so that it may be shared 59 // across multiple snapshots. 60 type workspace struct { 61 root span.URI 62 excludePath func(string) bool 63 moduleSource workspaceSource 64 65 // activeModFiles holds the active go.mod files. 66 activeModFiles map[span.URI]struct{} 67 68 // knownModFiles holds the set of all go.mod files in the workspace. 69 // In all modes except for legacy, this is equivalent to modFiles. 70 knownModFiles map[span.URI]struct{} 71 72 // workFile, if nonEmpty, is the go.work file for the workspace. 73 workFile span.URI 74 75 // The workspace module is lazily re-built once after being invalidated. 76 // buildMu+built guards this reconstruction. 77 // 78 // file and wsDirs may be non-nil even if built == false, if they were copied 79 // from the previous workspace module version. In this case, they will be 80 // preserved if building fails. 81 buildMu sync.Mutex 82 built bool 83 buildErr error 84 mod *modfile.File 85 sum []byte 86 wsDirs map[span.URI]struct{} 87 } 88 89 // newWorkspace creates a new workspace at the given root directory, 90 // determining its module source based on the presence of a gopls.mod or 91 // go.work file, and the go111moduleOff and useWsModule settings. 92 // 93 // If useWsModule is set, the workspace may use a synthetic mod file replacing 94 // all modules in the root. 95 // 96 // If there is no active workspace file (a gopls.mod or go.work), newWorkspace 97 // scans the filesystem to find modules. 98 func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff bool, useWsModule bool) (*workspace, error) { 99 ws := &workspace{ 100 root: root, 101 excludePath: excludePath, 102 } 103 104 // The user may have a gopls.mod or go.work file that defines their 105 // workspace. 106 if err := loadExplicitWorkspaceFile(ctx, ws, fs); err == nil { 107 return ws, nil 108 } 109 110 // Otherwise, in all other modes, search for all of the go.mod files in the 111 // workspace. 112 knownModFiles, err := findModules(root, excludePath, 0) 113 if err != nil { 114 return nil, err 115 } 116 ws.knownModFiles = knownModFiles 117 118 switch { 119 case go111moduleOff: 120 ws.moduleSource = legacyWorkspace 121 case useWsModule: 122 ws.activeModFiles = knownModFiles 123 ws.moduleSource = fileSystemWorkspace 124 default: 125 ws.moduleSource = legacyWorkspace 126 activeModFiles, err := getLegacyModules(ctx, root, fs) 127 if err != nil { 128 return nil, err 129 } 130 ws.activeModFiles = activeModFiles 131 } 132 return ws, nil 133 } 134 135 // loadExplicitWorkspaceFile loads workspace information from go.work or 136 // gopls.mod files, setting the active modules, mod file, and module source 137 // accordingly. 138 func loadExplicitWorkspaceFile(ctx context.Context, ws *workspace, fs source.FileSource) error { 139 for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { 140 fh, err := fs.GetFile(ctx, uriForSource(ws.root, src)) 141 if err != nil { 142 return err 143 } 144 contents, err := fh.Read() 145 if err != nil { 146 continue 147 } 148 var file *modfile.File 149 var activeModFiles map[span.URI]struct{} 150 switch src { 151 case goWorkWorkspace: 152 file, activeModFiles, err = parseGoWork(ctx, ws.root, fh.URI(), contents, fs) 153 ws.workFile = fh.URI() 154 case goplsModWorkspace: 155 file, activeModFiles, err = parseGoplsMod(ws.root, fh.URI(), contents) 156 } 157 if err != nil { 158 ws.buildMu.Lock() 159 ws.built = true 160 ws.buildErr = err 161 ws.buildMu.Unlock() 162 } 163 ws.mod = file 164 ws.activeModFiles = activeModFiles 165 ws.moduleSource = src 166 return nil 167 } 168 return noHardcodedWorkspace 169 } 170 171 var noHardcodedWorkspace = errors.New("no hardcoded workspace") 172 173 func (w *workspace) getKnownModFiles() map[span.URI]struct{} { 174 return w.knownModFiles 175 } 176 177 func (w *workspace) getActiveModFiles() map[span.URI]struct{} { 178 return w.activeModFiles 179 } 180 181 // modFile gets the workspace modfile associated with this workspace, 182 // computing it if it doesn't exist. 183 // 184 // A fileSource must be passed in to solve a chicken-egg problem: it is not 185 // correct to pass in the snapshot file source to newWorkspace when 186 // invalidating, because at the time these are called the snapshot is locked. 187 // So we must pass it in later on when actually using the modFile. 188 func (w *workspace) modFile(ctx context.Context, fs source.FileSource) (*modfile.File, error) { 189 w.build(ctx, fs) 190 return w.mod, w.buildErr 191 } 192 193 func (w *workspace) sumFile(ctx context.Context, fs source.FileSource) ([]byte, error) { 194 w.build(ctx, fs) 195 return w.sum, w.buildErr 196 } 197 198 func (w *workspace) build(ctx context.Context, fs source.FileSource) { 199 w.buildMu.Lock() 200 defer w.buildMu.Unlock() 201 202 if w.built { 203 return 204 } 205 // Building should never be cancelled. Since the workspace module is shared 206 // across multiple snapshots, doing so would put us in a bad state, and it 207 // would not be obvious to the user how to recover. 208 ctx = xcontext.Detach(ctx) 209 210 // If our module source is not gopls.mod, try to build the workspace module 211 // from modules. Fall back on the pre-existing mod file if parsing fails. 212 if w.moduleSource != goplsModWorkspace { 213 file, err := buildWorkspaceModFile(ctx, w.activeModFiles, fs) 214 switch { 215 case err == nil: 216 w.mod = file 217 case w.mod != nil: 218 // Parsing failed, but we have a previous file version. 219 event.Error(ctx, "building workspace mod file", err) 220 default: 221 // No file to fall back on. 222 w.buildErr = err 223 } 224 } 225 if w.mod != nil { 226 w.wsDirs = map[span.URI]struct{}{ 227 w.root: {}, 228 } 229 for _, r := range w.mod.Replace { 230 // We may be replacing a module with a different version, not a path 231 // on disk. 232 if r.New.Version != "" { 233 continue 234 } 235 w.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{} 236 } 237 } 238 // Ensure that there is always at least the root dir. 239 if len(w.wsDirs) == 0 { 240 w.wsDirs = map[span.URI]struct{}{ 241 w.root: {}, 242 } 243 } 244 sum, err := buildWorkspaceSumFile(ctx, w.activeModFiles, fs) 245 if err == nil { 246 w.sum = sum 247 } else { 248 event.Error(ctx, "building workspace sum file", err) 249 } 250 w.built = true 251 } 252 253 // dirs returns the workspace directories for the loaded modules. 254 func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { 255 w.build(ctx, fs) 256 var dirs []span.URI 257 for d := range w.wsDirs { 258 dirs = append(dirs, d) 259 } 260 sort.Slice(dirs, func(i, j int) bool { 261 return source.CompareURI(dirs[i], dirs[j]) < 0 262 }) 263 return dirs 264 } 265 266 // invalidate returns a (possibly) new workspace after invalidating the changed 267 // files. If w is still valid in the presence of changedURIs, it returns itself 268 // unmodified. 269 // 270 // The returned changed and reload flags control the level of invalidation. 271 // Some workspace changes may affect workspace contents without requiring a 272 // reload of metadata (for example, unsaved changes to a go.mod or go.sum 273 // file). 274 func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, changed, reload bool) { 275 // Prevent races to w.modFile or w.wsDirs below, if w has not yet been built. 276 w.buildMu.Lock() 277 defer w.buildMu.Unlock() 278 279 // Clone the workspace. This may be discarded if nothing changed. 280 result := &workspace{ 281 root: w.root, 282 moduleSource: w.moduleSource, 283 knownModFiles: make(map[span.URI]struct{}), 284 activeModFiles: make(map[span.URI]struct{}), 285 workFile: w.workFile, 286 mod: w.mod, 287 sum: w.sum, 288 wsDirs: w.wsDirs, 289 excludePath: w.excludePath, 290 } 291 for k, v := range w.knownModFiles { 292 result.knownModFiles[k] = v 293 } 294 for k, v := range w.activeModFiles { 295 result.activeModFiles[k] = v 296 } 297 298 // First handle changes to the go.work or gopls.mod file. This must be 299 // considered before any changes to go.mod or go.sum files, as these files 300 // determine which modules we care about. If go.work/gopls.mod has changed 301 // we need to either re-read it if it exists or walk the filesystem if it 302 // has been deleted. go.work should override the gopls.mod if both exist. 303 changed, reload = handleWorkspaceFileChanges(ctx, result, changes, fs) 304 // Next, handle go.mod changes that could affect our workspace. 305 for uri, change := range changes { 306 // Otherwise, we only care about go.mod files in the workspace directory. 307 if change.isUnchanged || !isGoMod(uri) || !source.InDir(result.root.Filename(), uri.Filename()) { 308 continue 309 } 310 changed = true 311 active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0 312 reload = reload || (active && change.fileHandle.Saved()) 313 // Don't mess with the list of mod files if using go.work or gopls.mod. 314 if result.moduleSource == goplsModWorkspace || result.moduleSource == goWorkWorkspace { 315 continue 316 } 317 if change.exists { 318 result.knownModFiles[uri] = struct{}{} 319 if active { 320 result.activeModFiles[uri] = struct{}{} 321 } 322 } else { 323 delete(result.knownModFiles, uri) 324 delete(result.activeModFiles, uri) 325 } 326 } 327 328 // Finally, process go.sum changes for any modules that are now active. 329 for uri, change := range changes { 330 if !isGoSum(uri) { 331 continue 332 } 333 // TODO(rFindley) factor out this URI mangling. 334 dir := filepath.Dir(uri.Filename()) 335 modURI := span.URIFromPath(filepath.Join(dir, "go.mod")) 336 if _, active := result.activeModFiles[modURI]; !active { 337 continue 338 } 339 // Only changes to active go.sum files actually cause the workspace to 340 // change. 341 changed = true 342 reload = reload || change.fileHandle.Saved() 343 } 344 345 if !changed { 346 return w, false, false 347 } 348 349 return result, changed, reload 350 } 351 352 // handleWorkspaceFileChanges handles changes related to a go.work or gopls.mod 353 // file, updating ws accordingly. ws.root must be set. 354 func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[span.URI]*fileChange, fs source.FileSource) (changed, reload bool) { 355 // If go.work/gopls.mod has changed we need to either re-read it if it 356 // exists or walk the filesystem if it has been deleted. 357 // go.work should override the gopls.mod if both exist. 358 for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { 359 uri := uriForSource(ws.root, src) 360 // File opens/closes are just no-ops. 361 change, ok := changes[uri] 362 if !ok { 363 continue 364 } 365 if change.isUnchanged { 366 break 367 } 368 if change.exists { 369 // Only invalidate if the file if it actually parses. 370 // Otherwise, stick with the current file. 371 var parsedFile *modfile.File 372 var parsedModules map[span.URI]struct{} 373 var err error 374 switch src { 375 case goWorkWorkspace: 376 parsedFile, parsedModules, err = parseGoWork(ctx, ws.root, uri, change.content, fs) 377 case goplsModWorkspace: 378 parsedFile, parsedModules, err = parseGoplsMod(ws.root, uri, change.content) 379 } 380 if err != nil { 381 // An unparseable file should not invalidate the workspace: 382 // nothing good could come from changing the workspace in 383 // this case. 384 event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err) 385 } else { 386 // only update the modfile if it parsed. 387 changed = true 388 reload = change.fileHandle.Saved() 389 ws.mod = parsedFile 390 ws.moduleSource = src 391 ws.knownModFiles = parsedModules 392 ws.activeModFiles = make(map[span.URI]struct{}) 393 for k, v := range parsedModules { 394 ws.activeModFiles[k] = v 395 } 396 } 397 break // We've found an explicit workspace file, so can stop looking. 398 } else { 399 // go.work/gopls.mod is deleted. search for modules again. 400 changed = true 401 reload = true 402 ws.moduleSource = fileSystemWorkspace 403 // The parsed file is no longer valid. 404 ws.mod = nil 405 knownModFiles, err := findModules(ws.root, ws.excludePath, 0) 406 if err != nil { 407 ws.knownModFiles = nil 408 ws.activeModFiles = nil 409 event.Error(ctx, "finding file system modules", err) 410 } else { 411 ws.knownModFiles = knownModFiles 412 ws.activeModFiles = make(map[span.URI]struct{}) 413 for k, v := range ws.knownModFiles { 414 ws.activeModFiles[k] = v 415 } 416 } 417 } 418 } 419 return changed, reload 420 } 421 422 // goplsModURI returns the URI for the gopls.mod file contained in root. 423 func uriForSource(root span.URI, src workspaceSource) span.URI { 424 var basename string 425 switch src { 426 case goplsModWorkspace: 427 basename = "gopls.mod" 428 case goWorkWorkspace: 429 basename = "go.work" 430 default: 431 return "" 432 } 433 return span.URIFromPath(filepath.Join(root.Filename(), basename)) 434 } 435 436 // modURI returns the URI for the go.mod file contained in root. 437 func modURI(root span.URI) span.URI { 438 return span.URIFromPath(filepath.Join(root.Filename(), "go.mod")) 439 } 440 441 // isGoMod reports if uri is a go.mod file. 442 func isGoMod(uri span.URI) bool { 443 return filepath.Base(uri.Filename()) == "go.mod" 444 } 445 446 func isGoSum(uri span.URI) bool { 447 return filepath.Base(uri.Filename()) == "go.sum" || filepath.Base(uri.Filename()) == "go.work.sum" 448 } 449 450 // fileExists reports if the file uri exists within source. 451 func fileExists(ctx context.Context, uri span.URI, source source.FileSource) (bool, error) { 452 fh, err := source.GetFile(ctx, uri) 453 if err != nil { 454 return false, err 455 } 456 return fileHandleExists(fh) 457 } 458 459 // fileHandleExists reports if the file underlying fh actually exits. 460 func fileHandleExists(fh source.FileHandle) (bool, error) { 461 _, err := fh.Read() 462 if err == nil { 463 return true, nil 464 } 465 if os.IsNotExist(err) { 466 return false, nil 467 } 468 return false, err 469 } 470 471 // TODO(rFindley): replace this (and similar) with a uripath package analogous 472 // to filepath. 473 func dirURI(uri span.URI) span.URI { 474 return span.URIFromPath(filepath.Dir(uri.Filename())) 475 } 476 477 // getLegacyModules returns a module set containing at most the root module. 478 func getLegacyModules(ctx context.Context, root span.URI, fs source.FileSource) (map[span.URI]struct{}, error) { 479 uri := span.URIFromPath(filepath.Join(root.Filename(), "go.mod")) 480 modules := make(map[span.URI]struct{}) 481 exists, err := fileExists(ctx, uri, fs) 482 if err != nil { 483 return nil, err 484 } 485 if exists { 486 modules[uri] = struct{}{} 487 } 488 return modules, nil 489 } 490 491 func parseGoWork(ctx context.Context, root, uri span.URI, contents []byte, fs source.FileSource) (*modfile.File, map[span.URI]struct{}, error) { 492 workFile, err := modfile.ParseWork(uri.Filename(), contents, nil) 493 if err != nil { 494 return nil, nil, errors.Errorf("parsing go.work: %w", err) 495 } 496 modFiles := make(map[span.URI]struct{}) 497 for _, dir := range workFile.Use { 498 // The resulting modfile must use absolute paths, so that it can be 499 // written to a temp directory. 500 dir.Path = absolutePath(root, dir.Path) 501 modURI := span.URIFromPath(filepath.Join(dir.Path, "go.mod")) 502 modFiles[modURI] = struct{}{} 503 } 504 modFile, err := buildWorkspaceModFile(ctx, modFiles, fs) 505 if err != nil { 506 return nil, nil, err 507 } 508 509 // Require a go directive, per the spec. 510 if workFile.Go == nil || workFile.Go.Version == "" { 511 return nil, nil, fmt.Errorf("go.work has missing or incomplete go directive") 512 } 513 if err := modFile.AddGoStmt(workFile.Go.Version); err != nil { 514 return nil, nil, err 515 } 516 517 return modFile, modFiles, nil 518 } 519 520 func parseGoplsMod(root, uri span.URI, contents []byte) (*modfile.File, map[span.URI]struct{}, error) { 521 modFile, err := modfile.Parse(uri.Filename(), contents, nil) 522 if err != nil { 523 return nil, nil, errors.Errorf("parsing gopls.mod: %w", err) 524 } 525 modFiles := make(map[span.URI]struct{}) 526 for _, replace := range modFile.Replace { 527 if replace.New.Version != "" { 528 return nil, nil, errors.Errorf("gopls.mod: replaced module %q@%q must not have version", replace.New.Path, replace.New.Version) 529 } 530 // The resulting modfile must use absolute paths, so that it can be 531 // written to a temp directory. 532 replace.New.Path = absolutePath(root, replace.New.Path) 533 modURI := span.URIFromPath(filepath.Join(replace.New.Path, "go.mod")) 534 modFiles[modURI] = struct{}{} 535 } 536 return modFile, modFiles, nil 537 } 538 539 func absolutePath(root span.URI, path string) string { 540 dirFP := filepath.FromSlash(path) 541 if !filepath.IsAbs(dirFP) { 542 dirFP = filepath.Join(root.Filename(), dirFP) 543 } 544 return dirFP 545 } 546 547 // errExhausted is returned by findModules if the file scan limit is reached. 548 var errExhausted = errors.New("exhausted") 549 550 // Limit go.mod search to 1 million files. As a point of reference, 551 // Kubernetes has 22K files (as of 2020-11-24). 552 const fileLimit = 1000000 553 554 // findModules recursively walks the root directory looking for go.mod files, 555 // returning the set of modules it discovers. If modLimit is non-zero, 556 // searching stops once modLimit modules have been found. 557 // 558 // TODO(rfindley): consider overlays. 559 func findModules(root span.URI, excludePath func(string) bool, modLimit int) (map[span.URI]struct{}, error) { 560 // Walk the view's folder to find all modules in the view. 561 modFiles := make(map[span.URI]struct{}) 562 searched := 0 563 errDone := errors.New("done") 564 err := filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error { 565 if err != nil { 566 // Probably a permission error. Keep looking. 567 return filepath.SkipDir 568 } 569 // For any path that is not the workspace folder, check if the path 570 // would be ignored by the go command. Vendor directories also do not 571 // contain workspace modules. 572 if info.IsDir() && path != root.Filename() { 573 suffix := strings.TrimPrefix(path, root.Filename()) 574 switch { 575 case checkIgnored(suffix), 576 strings.Contains(filepath.ToSlash(suffix), "/vendor/"), 577 excludePath(suffix): 578 return filepath.SkipDir 579 } 580 } 581 // We're only interested in go.mod files. 582 uri := span.URIFromPath(path) 583 if isGoMod(uri) { 584 modFiles[uri] = struct{}{} 585 } 586 if modLimit > 0 && len(modFiles) >= modLimit { 587 return errDone 588 } 589 searched++ 590 if fileLimit > 0 && searched >= fileLimit { 591 return errExhausted 592 } 593 return nil 594 }) 595 if err == errDone { 596 return modFiles, nil 597 } 598 return modFiles, err 599 }