github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/source/view.go (about) 1 // Copyright 2018 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 source 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "go/ast" 12 "go/scanner" 13 "go/token" 14 "go/types" 15 "io" 16 "strings" 17 18 "golang.org/x/mod/modfile" 19 "golang.org/x/mod/module" 20 "github.com/jhump/golang-x-tools/go/analysis" 21 "github.com/jhump/golang-x-tools/internal/gocommand" 22 "github.com/jhump/golang-x-tools/internal/imports" 23 "github.com/jhump/golang-x-tools/internal/lsp/progress" 24 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 25 "github.com/jhump/golang-x-tools/internal/span" 26 errors "golang.org/x/xerrors" 27 ) 28 29 // Snapshot represents the current state for the given view. 30 type Snapshot interface { 31 ID() uint64 32 33 // View returns the View associated with this snapshot. 34 View() View 35 36 // BackgroundContext returns a context used for all background processing 37 // on behalf of this snapshot. 38 BackgroundContext() context.Context 39 40 // Fileset returns the Fileset used to parse all the Go files in this snapshot. 41 FileSet() *token.FileSet 42 43 // ValidBuildConfiguration returns true if there is some error in the 44 // user's workspace. In particular, if they are both outside of a module 45 // and their GOPATH. 46 ValidBuildConfiguration() bool 47 48 // WriteEnv writes the view-specific environment to the io.Writer. 49 WriteEnv(ctx context.Context, w io.Writer) error 50 51 // FindFile returns the FileHandle for the given URI, if it is already 52 // in the given snapshot. 53 FindFile(uri span.URI) VersionedFileHandle 54 55 // GetVersionedFile returns the VersionedFileHandle for a given URI, 56 // initializing it if it is not already part of the snapshot. 57 GetVersionedFile(ctx context.Context, uri span.URI) (VersionedFileHandle, error) 58 59 // GetFile returns the FileHandle for a given URI, initializing it if it is 60 // not already part of the snapshot. 61 GetFile(ctx context.Context, uri span.URI) (FileHandle, error) 62 63 // AwaitInitialized waits until the snapshot's view is initialized. 64 AwaitInitialized(ctx context.Context) 65 66 // IsOpen returns whether the editor currently has a file open. 67 IsOpen(uri span.URI) bool 68 69 // IgnoredFile reports if a file would be ignored by a `go list` of the whole 70 // workspace. 71 IgnoredFile(uri span.URI) bool 72 73 // Templates returns the .tmpl files 74 Templates() map[span.URI]VersionedFileHandle 75 76 // ParseGo returns the parsed AST for the file. 77 // If the file is not available, returns nil and an error. 78 ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error) 79 80 // PosToField is a cache of *ast.Fields by token.Pos. This allows us 81 // to quickly find corresponding *ast.Field node given a *types.Var. 82 // We must refer to the AST to render type aliases properly when 83 // formatting signatures and other types. 84 PosToField(ctx context.Context, pkg Package, pos token.Pos) (*ast.Field, error) 85 86 // PosToDecl maps certain objects' positions to their surrounding 87 // ast.Decl. This mapping is used when building the documentation 88 // string for the objects. 89 PosToDecl(ctx context.Context, pkg Package, pos token.Pos) (ast.Decl, error) 90 91 // DiagnosePackage returns basic diagnostics, including list, parse, and type errors 92 // for pkg, grouped by file. 93 DiagnosePackage(ctx context.Context, pkg Package) (map[span.URI][]*Diagnostic, error) 94 95 // Analyze runs the analyses for the given package at this snapshot. 96 Analyze(ctx context.Context, pkgID string, analyzers []*Analyzer) ([]*Diagnostic, error) 97 98 // RunGoCommandPiped runs the given `go` command, writing its output 99 // to stdout and stderr. Verb, Args, and WorkingDir must be specified. 100 RunGoCommandPiped(ctx context.Context, mode InvocationFlags, inv *gocommand.Invocation, stdout, stderr io.Writer) error 101 102 // RunGoCommandDirect runs the given `go` command. Verb, Args, and 103 // WorkingDir must be specified. 104 RunGoCommandDirect(ctx context.Context, mode InvocationFlags, inv *gocommand.Invocation) (*bytes.Buffer, error) 105 106 // RunGoCommands runs a series of `go` commands that updates the go.mod 107 // and go.sum file for wd, and returns their updated contents. 108 RunGoCommands(ctx context.Context, allowNetwork bool, wd string, run func(invoke func(...string) (*bytes.Buffer, error)) error) (bool, []byte, []byte, error) 109 110 // RunProcessEnvFunc runs fn with the process env for this snapshot's view. 111 // Note: the process env contains cached module and filesystem state. 112 RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error 113 114 // ModFiles are the go.mod files enclosed in the snapshot's view and known 115 // to the snapshot. 116 ModFiles() []span.URI 117 118 // ParseMod is used to parse go.mod files. 119 ParseMod(ctx context.Context, fh FileHandle) (*ParsedModule, error) 120 121 // ModWhy returns the results of `go mod why` for the module specified by 122 // the given go.mod file. 123 ModWhy(ctx context.Context, fh FileHandle) (map[string]string, error) 124 125 // ModTidy returns the results of `go mod tidy` for the module specified by 126 // the given go.mod file. 127 ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error) 128 129 // GoModForFile returns the URI of the go.mod file for the given URI. 130 GoModForFile(uri span.URI) span.URI 131 132 // ParseWork is used to parse go.work files. 133 ParseWork(ctx context.Context, fh FileHandle) (*ParsedWorkFile, error) 134 135 // BuiltinFile returns information about the special builtin package. 136 BuiltinFile(ctx context.Context) (*ParsedGoFile, error) 137 138 // IsBuiltin reports whether uri is part of the builtin package. 139 IsBuiltin(ctx context.Context, uri span.URI) bool 140 141 // PackagesForFile returns the packages that this file belongs to, checked 142 // in mode. 143 PackagesForFile(ctx context.Context, uri span.URI, mode TypecheckMode, includeTestVariants bool) ([]Package, error) 144 145 // PackageForFile returns a single package that this file belongs to, 146 // checked in mode and filtered by the package policy. 147 PackageForFile(ctx context.Context, uri span.URI, mode TypecheckMode, selectPackage PackageFilter) (Package, error) 148 149 // GetActiveReverseDeps returns the active files belonging to the reverse 150 // dependencies of this file's package, checked in TypecheckWorkspace mode. 151 GetReverseDependencies(ctx context.Context, id string) ([]Package, error) 152 153 // CachedImportPaths returns all the imported packages loaded in this 154 // snapshot, indexed by their import path and checked in TypecheckWorkspace 155 // mode. 156 CachedImportPaths(ctx context.Context) (map[string]Package, error) 157 158 // KnownPackages returns all the packages loaded in this snapshot, checked 159 // in TypecheckWorkspace mode. 160 KnownPackages(ctx context.Context) ([]Package, error) 161 162 // ActivePackages returns the packages considered 'active' in the workspace. 163 // 164 // In normal memory mode, this is all workspace packages. In degraded memory 165 // mode, this is just the reverse transitive closure of open packages. 166 ActivePackages(ctx context.Context) ([]Package, error) 167 168 // Symbols returns all symbols in the snapshot. 169 Symbols(ctx context.Context) (map[span.URI][]Symbol, error) 170 171 // Metadata returns package metadata associated with the given file URI. 172 MetadataForFile(ctx context.Context, uri span.URI) ([]Metadata, error) 173 174 // GetCriticalError returns any critical errors in the workspace. 175 GetCriticalError(ctx context.Context) *CriticalError 176 177 // BuildGoplsMod generates a go.mod file for all modules in the workspace. 178 // It bypasses any existing gopls.mod. 179 BuildGoplsMod(ctx context.Context) (*modfile.File, error) 180 } 181 182 // PackageFilter sets how a package is filtered out from a set of packages 183 // containing a given file. 184 type PackageFilter int 185 186 const ( 187 // NarrowestPackage picks the "narrowest" package for a given file. 188 // By "narrowest" package, we mean the package with the fewest number of 189 // files that includes the given file. This solves the problem of test 190 // variants, as the test will have more files than the non-test package. 191 NarrowestPackage PackageFilter = iota 192 193 // WidestPackage returns the Package containing the most files. 194 // This is useful for something like diagnostics, where we'd prefer to 195 // offer diagnostics for as many files as possible. 196 WidestPackage 197 ) 198 199 // InvocationFlags represents the settings of a particular go command invocation. 200 // It is a mode, plus a set of flag bits. 201 type InvocationFlags int 202 203 const ( 204 // Normal is appropriate for commands that might be run by a user and don't 205 // deliberately modify go.mod files, e.g. `go test`. 206 Normal InvocationFlags = iota 207 // WriteTemporaryModFile is for commands that need information from a 208 // modified version of the user's go.mod file, e.g. `go mod tidy` used to 209 // generate diagnostics. 210 WriteTemporaryModFile 211 // LoadWorkspace is for packages.Load, and other operations that should 212 // consider the whole workspace at once. 213 LoadWorkspace 214 215 // AllowNetwork is a flag bit that indicates the invocation should be 216 // allowed to access the network. 217 AllowNetwork InvocationFlags = 1 << 10 218 ) 219 220 func (m InvocationFlags) Mode() InvocationFlags { 221 return m & (AllowNetwork - 1) 222 } 223 224 func (m InvocationFlags) AllowNetwork() bool { 225 return m&AllowNetwork != 0 226 } 227 228 // View represents a single workspace. 229 // This is the level at which we maintain configuration like working directory 230 // and build tags. 231 type View interface { 232 // Name returns the name this view was constructed with. 233 Name() string 234 235 // Folder returns the folder with which this view was created. 236 Folder() span.URI 237 238 // Shutdown closes this view, and detaches it from its session. 239 Shutdown(ctx context.Context) 240 241 // Options returns a copy of the Options for this view. 242 Options() *Options 243 244 // SetOptions sets the options of this view to new values. 245 // Calling this may cause the view to be invalidated and a replacement view 246 // added to the session. If so the new view will be returned, otherwise the 247 // original one will be. 248 SetOptions(context.Context, *Options) (View, error) 249 250 // Snapshot returns the current snapshot for the view. 251 Snapshot(ctx context.Context) (Snapshot, func()) 252 253 // Rebuild rebuilds the current view, replacing the original view in its session. 254 Rebuild(ctx context.Context) (Snapshot, func(), error) 255 256 // IsGoPrivatePath reports whether target is a private import path, as identified 257 // by the GOPRIVATE environment variable. 258 IsGoPrivatePath(path string) bool 259 260 // ModuleUpgrades returns known module upgrades. 261 ModuleUpgrades() map[string]string 262 263 // RegisterModuleUpgrades registers that upgrades exist for the given modules. 264 RegisterModuleUpgrades(upgrades map[string]string) 265 266 // FileKind returns the type of a file 267 FileKind(FileHandle) FileKind 268 } 269 270 // A FileSource maps uris to FileHandles. This abstraction exists both for 271 // testability, and so that algorithms can be run equally on session and 272 // snapshot files. 273 type FileSource interface { 274 // GetFile returns the FileHandle for a given URI. 275 GetFile(ctx context.Context, uri span.URI) (FileHandle, error) 276 } 277 278 // A ParsedGoFile contains the results of parsing a Go file. 279 type ParsedGoFile struct { 280 URI span.URI 281 Mode ParseMode 282 File *ast.File 283 Tok *token.File 284 // Source code used to build the AST. It may be different from the 285 // actual content of the file if we have fixed the AST. 286 Src []byte 287 Mapper *protocol.ColumnMapper 288 ParseErr scanner.ErrorList 289 } 290 291 // A ParsedModule contains the results of parsing a go.mod file. 292 type ParsedModule struct { 293 URI span.URI 294 File *modfile.File 295 Mapper *protocol.ColumnMapper 296 ParseErrors []*Diagnostic 297 } 298 299 // A ParsedWorkFile contains the results of parsing a go.work file. 300 type ParsedWorkFile struct { 301 URI span.URI 302 File *modfile.WorkFile 303 Mapper *protocol.ColumnMapper 304 ParseErrors []*Diagnostic 305 } 306 307 // A TidiedModule contains the results of running `go mod tidy` on a module. 308 type TidiedModule struct { 309 // Diagnostics representing changes made by `go mod tidy`. 310 Diagnostics []*Diagnostic 311 // The bytes of the go.mod file after it was tidied. 312 TidiedContent []byte 313 } 314 315 // Metadata represents package metadata retrieved from go/packages. 316 type Metadata interface { 317 // PackageName is the package name. 318 PackageName() string 319 320 // PackagePath is the package path. 321 PackagePath() string 322 } 323 324 // Session represents a single connection from a client. 325 // This is the level at which things like open files are maintained on behalf 326 // of the client. 327 // A session may have many active views at any given time. 328 type Session interface { 329 // ID returns the unique identifier for this session on this server. 330 ID() string 331 // NewView creates a new View, returning it and its first snapshot. If a 332 // non-empty tempWorkspace directory is provided, the View will record a copy 333 // of its gopls workspace module in that directory, so that client tooling 334 // can execute in the same main module. 335 NewView(ctx context.Context, name string, folder span.URI, options *Options) (View, Snapshot, func(), error) 336 337 // Cache returns the cache that created this session, for debugging only. 338 Cache() interface{} 339 340 // View returns a view with a matching name, if the session has one. 341 View(name string) View 342 343 // ViewOf returns a view corresponding to the given URI. 344 ViewOf(uri span.URI) (View, error) 345 346 // Views returns the set of active views built by this session. 347 Views() []View 348 349 // Shutdown the session and all views it has created. 350 Shutdown(ctx context.Context) 351 352 // GetFile returns a handle for the specified file. 353 GetFile(ctx context.Context, uri span.URI) (FileHandle, error) 354 355 // DidModifyFile reports a file modification to the session. It returns 356 // the new snapshots after the modifications have been applied, paired with 357 // the affected file URIs for those snapshots. 358 DidModifyFiles(ctx context.Context, changes []FileModification) (map[Snapshot][]span.URI, []func(), error) 359 360 // ExpandModificationsToDirectories returns the set of changes with the 361 // directory changes removed and expanded to include all of the files in 362 // the directory. 363 ExpandModificationsToDirectories(ctx context.Context, changes []FileModification) []FileModification 364 365 // Overlays returns a slice of file overlays for the session. 366 Overlays() []Overlay 367 368 // Options returns a copy of the SessionOptions for this session. 369 Options() *Options 370 371 // SetOptions sets the options of this session to new values. 372 SetOptions(*Options) 373 374 // FileWatchingGlobPatterns returns glob patterns to watch every directory 375 // known by the view. For views within a module, this is the module root, 376 // any directory in the module root, and any replace targets. 377 FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} 378 379 // SetProgressTracker sets the progress tracker for the session. 380 SetProgressTracker(tracker *progress.Tracker) 381 } 382 383 var ErrViewExists = errors.New("view already exists for session") 384 385 // Overlay is the type for a file held in memory on a session. 386 type Overlay interface { 387 Kind() FileKind 388 VersionedFileHandle 389 } 390 391 // FileModification represents a modification to a file. 392 type FileModification struct { 393 URI span.URI 394 Action FileAction 395 396 // OnDisk is true if a watched file is changed on disk. 397 // If true, Version will be -1 and Text will be nil. 398 OnDisk bool 399 400 // Version will be -1 and Text will be nil when they are not supplied, 401 // specifically on textDocument/didClose and for on-disk changes. 402 Version int32 403 Text []byte 404 405 // LanguageID is only sent from the language client on textDocument/didOpen. 406 LanguageID string 407 } 408 409 type FileAction int 410 411 const ( 412 UnknownFileAction = FileAction(iota) 413 Open 414 Change 415 Close 416 Save 417 Create 418 Delete 419 InvalidateMetadata 420 ) 421 422 func (a FileAction) String() string { 423 switch a { 424 case Open: 425 return "Open" 426 case Change: 427 return "Change" 428 case Close: 429 return "Close" 430 case Save: 431 return "Save" 432 case Create: 433 return "Create" 434 case Delete: 435 return "Delete" 436 case InvalidateMetadata: 437 return "InvalidateMetadata" 438 default: 439 return "Unknown" 440 } 441 } 442 443 var ErrTmpModfileUnsupported = errors.New("-modfile is unsupported for this Go version") 444 var ErrNoModOnDisk = errors.New("go.mod file is not on disk") 445 446 func IsNonFatalGoModError(err error) bool { 447 return err == ErrTmpModfileUnsupported || err == ErrNoModOnDisk 448 } 449 450 // ParseMode controls the content of the AST produced when parsing a source file. 451 type ParseMode int 452 453 const ( 454 // ParseHeader specifies that the main package declaration and imports are needed. 455 // This is the mode used when attempting to examine the package graph structure. 456 ParseHeader ParseMode = iota 457 458 // ParseExported specifies that the package is used only as a dependency, 459 // and only its exported declarations are needed. More may be included if 460 // necessary to avoid type errors. 461 ParseExported 462 463 // ParseFull specifies the full AST is needed. 464 // This is used for files of direct interest where the entire contents must 465 // be considered. 466 ParseFull 467 ) 468 469 // TypecheckMode controls what kind of parsing should be done (see ParseMode) 470 // while type checking a package. 471 type TypecheckMode int 472 473 const ( 474 // Invalid default value. 475 TypecheckUnknown TypecheckMode = iota 476 // TypecheckFull means to use ParseFull. 477 TypecheckFull 478 // TypecheckWorkspace means to use ParseFull for workspace packages, and 479 // ParseExported for others. 480 TypecheckWorkspace 481 // TypecheckAll means ParseFull for workspace packages, and both Full and 482 // Exported for others. Only valid for some functions. 483 TypecheckAll 484 ) 485 486 type VersionedFileHandle interface { 487 FileHandle 488 Version() int32 489 Session() string 490 491 // LSPIdentity returns the version identity of a file. 492 VersionedFileIdentity() VersionedFileIdentity 493 } 494 495 type VersionedFileIdentity struct { 496 URI span.URI 497 498 // SessionID is the ID of the LSP session. 499 SessionID string 500 501 // Version is the version of the file, as specified by the client. It should 502 // only be set in combination with SessionID. 503 Version int32 504 } 505 506 // FileHandle represents a handle to a specific version of a single file. 507 type FileHandle interface { 508 URI() span.URI 509 510 // FileIdentity returns a FileIdentity for the file, even if there was an 511 // error reading it. 512 FileIdentity() FileIdentity 513 // Read reads the contents of a file. 514 // If the file is not available, returns a nil slice and an error. 515 Read() ([]byte, error) 516 // Saved reports whether the file has the same content on disk. 517 Saved() bool 518 } 519 520 // FileIdentity uniquely identifies a file at a version from a FileSystem. 521 type FileIdentity struct { 522 URI span.URI 523 524 // Identifier represents a unique identifier for the file's content. 525 Hash string 526 } 527 528 func (id FileIdentity) String() string { 529 return fmt.Sprintf("%s%s", id.URI, id.Hash) 530 } 531 532 // FileKind describes the kind of the file in question. 533 // It can be one of Go,mod, Sum, or Tmpl. 534 type FileKind int 535 536 const ( 537 // UnknownKind is a file type we don't know about. 538 UnknownKind = FileKind(iota) 539 540 // Go is a normal go source file. 541 Go 542 // Mod is a go.mod file. 543 Mod 544 // Sum is a go.sum file. 545 Sum 546 // Tmpl is a template file. 547 Tmpl 548 // Work is a go.work file. 549 Work 550 ) 551 552 // Analyzer represents a go/analysis analyzer with some boolean properties 553 // that let the user know how to use the analyzer. 554 type Analyzer struct { 555 Analyzer *analysis.Analyzer 556 557 // Enabled reports whether the analyzer is enabled. This value can be 558 // configured per-analysis in user settings. For staticcheck analyzers, 559 // the value of the Staticcheck setting overrides this field. 560 Enabled bool 561 562 // Fix is the name of the suggested fix name used to invoke the suggested 563 // fixes for the analyzer. It is non-empty if we expect this analyzer to 564 // provide its fix separately from its diagnostics. That is, we should apply 565 // the analyzer's suggested fixes through a Command, not a TextEdit. 566 Fix string 567 568 // ActionKind is the kind of code action this analyzer produces. If 569 // unspecified the type defaults to quickfix. 570 ActionKind []protocol.CodeActionKind 571 572 // Severity is the severity set for diagnostics reported by this 573 // analyzer. If left unset it defaults to Warning. 574 Severity protocol.DiagnosticSeverity 575 } 576 577 func (a Analyzer) IsEnabled(view View) bool { 578 // Staticcheck analyzers can only be enabled when staticcheck is on. 579 if _, ok := view.Options().StaticcheckAnalyzers[a.Analyzer.Name]; ok { 580 if !view.Options().Staticcheck { 581 return false 582 } 583 } 584 if enabled, ok := view.Options().Analyses[a.Analyzer.Name]; ok { 585 return enabled 586 } 587 return a.Enabled 588 } 589 590 // Package represents a Go package that has been type-checked. It maintains 591 // only the relevant fields of a *go/packages.Package. 592 type Package interface { 593 ID() string 594 Name() string 595 PkgPath() string 596 CompiledGoFiles() []*ParsedGoFile 597 File(uri span.URI) (*ParsedGoFile, error) 598 GetSyntax() []*ast.File 599 GetTypes() *types.Package 600 GetTypesInfo() *types.Info 601 GetTypesSizes() types.Sizes 602 IsIllTyped() bool 603 ForTest() string 604 GetImport(pkgPath string) (Package, error) 605 MissingDependencies() []string 606 Imports() []Package 607 Version() *module.Version 608 HasListOrParseErrors() bool 609 HasTypeErrors() bool 610 ParseMode() ParseMode 611 } 612 613 type CriticalError struct { 614 // MainError is the primary error. Must be non-nil. 615 MainError error 616 // DiagList contains any supplemental (structured) diagnostics. 617 DiagList []*Diagnostic 618 } 619 620 // An Diagnostic corresponds to an LSP Diagnostic. 621 // https://microsoft.github.io/language-server-protocol/specification#diagnostic 622 type Diagnostic struct { 623 URI span.URI 624 Range protocol.Range 625 Severity protocol.DiagnosticSeverity 626 Code string 627 CodeHref string 628 629 // Source is a human-readable description of the source of the error. 630 // Diagnostics generated by an analysis.Analyzer set it to Analyzer.Name. 631 Source DiagnosticSource 632 633 Message string 634 635 Tags []protocol.DiagnosticTag 636 Related []RelatedInformation 637 638 // Fields below are used internally to generate quick fixes. They aren't 639 // part of the LSP spec and don't leave the server. 640 SuggestedFixes []SuggestedFix 641 Analyzer *Analyzer 642 } 643 644 type DiagnosticSource string 645 646 const ( 647 UnknownError DiagnosticSource = "<Unknown source>" 648 ListError DiagnosticSource = "go list" 649 ParseError DiagnosticSource = "syntax" 650 TypeError DiagnosticSource = "compiler" 651 ModTidyError DiagnosticSource = "go mod tidy" 652 OptimizationDetailsError DiagnosticSource = "optimizer details" 653 UpgradeNotification DiagnosticSource = "upgrade available" 654 TemplateError DiagnosticSource = "template" 655 ) 656 657 func AnalyzerErrorKind(name string) DiagnosticSource { 658 return DiagnosticSource(name) 659 } 660 661 var ( 662 PackagesLoadError = errors.New("packages.Load error") 663 ) 664 665 // WorkspaceModuleVersion is the nonexistent pseudoversion suffix used in the 666 // construction of the workspace module. It is exported so that we can make 667 // sure not to show this version to end users in error messages, to avoid 668 // confusion. 669 // The major version is not included, as that depends on the module path. 670 // 671 // If workspace module A is dependent on workspace module B, we need our 672 // nonexistant version to be greater than the version A mentions. 673 // Otherwise, the go command will try to update to that version. Use a very 674 // high minor version to make that more likely. 675 const workspaceModuleVersion = ".9999999.0-goplsworkspace" 676 677 func IsWorkspaceModuleVersion(version string) bool { 678 return strings.HasSuffix(version, workspaceModuleVersion) 679 } 680 681 func WorkspaceModuleVersion(majorVersion string) string { 682 // Use the highest compatible major version to avoid unwanted upgrades. 683 // See the comment on workspaceModuleVersion. 684 if majorVersion == "v0" { 685 majorVersion = "v1" 686 } 687 return majorVersion + workspaceModuleVersion 688 }