1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804
|
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cache
import (
"bytes"
"context"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"sync/atomic"
"time"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/label"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/immutable"
"golang.org/x/tools/gopls/internal/util/pathutil"
"golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/xcontext"
)
var loadID uint64 // atomic identifier for loads
// errNoPackages indicates that a load query matched no packages.
var errNoPackages = errors.New("no packages returned")
// load calls packages.Load for the given scopes, updating package metadata,
// import graph, and mapped files with the result.
//
// The resulting error may wrap the moduleErrorMap error type, representing
// errors associated with specific modules.
//
// If scopes contains a file scope there must be exactly one scope.
func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) {
if ctx.Err() != nil {
// Check context cancellation before incrementing id below: a load on a
// cancelled context should be a no-op.
return ctx.Err()
}
id := atomic.AddUint64(&loadID, 1)
eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging
var query []string
var standalone bool // whether this is a load of a standalone file
// Keep track of module query -> module path so that we can later correlate query
// errors with errors.
moduleQueries := make(map[string]string)
for _, scope := range scopes {
switch scope := scope.(type) {
case packageLoadScope:
// The only time we pass package paths is when we're doing a
// partial workspace load. In those cases, the paths came back from
// go list and should already be GOPATH-vendorized when appropriate.
query = append(query, string(scope))
case fileLoadScope:
// Given multiple scopes, the resulting load might contain inaccurate
// information. For example go/packages returns at most one command-line
// arguments package, and does not handle a combination of standalone
// files and packages.
uri := protocol.DocumentURI(scope)
if len(scopes) > 1 {
panic(fmt.Sprintf("internal error: load called with multiple scopes when a file scope is present (file: %s)", uri))
}
fh := s.FindFile(uri)
if fh == nil || s.FileKind(fh) != file.Go {
// Don't try to load a file that doesn't exist, or isn't a go file.
continue
}
contents, err := fh.Content()
if err != nil {
continue
}
if isStandaloneFile(contents, s.Options().StandaloneTags) {
standalone = true
query = append(query, uri.Path())
} else {
query = append(query, fmt.Sprintf("file=%s", uri.Path()))
}
case moduleLoadScope:
modQuery := fmt.Sprintf("%s%c...", scope.dir, filepath.Separator)
query = append(query, modQuery)
moduleQueries[modQuery] = scope.modulePath
case viewLoadScope:
// If we are outside of GOPATH, a module, or some other known
// build system, don't load subdirectories.
if s.view.typ == AdHocView {
query = append(query, "./")
} else {
query = append(query, "./...")
}
default:
panic(fmt.Sprintf("unknown scope type %T", scope))
}
}
if len(query) == 0 {
return nil
}
sort.Strings(query) // for determinism
ctx, done := event.Start(ctx, "cache.snapshot.load", label.Query.Of(query))
defer done()
startTime := time.Now()
inv, cleanupInvocation, err := s.GoCommandInvocation(allowNetwork, &gocommand.Invocation{
WorkingDir: s.view.root.Path(),
})
if err != nil {
return err
}
defer cleanupInvocation()
// Set a last resort deadline on packages.Load since it calls the go
// command, which may hang indefinitely if it has a bug. golang/go#42132
// and golang/go#42255 have more context.
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
cfg := s.config(ctx, inv)
pkgs, err := packages.Load(cfg, query...)
// If the context was canceled, return early. Otherwise, we might be
// type-checking an incomplete result. Check the context directly,
// because go/packages adds extra information to the error.
if ctx.Err() != nil {
return ctx.Err()
}
// This log message is sought for by TestReloadOnlyOnce.
{
lbls := append(s.Labels(),
label.Query.Of(query),
label.PackageCount.Of(len(pkgs)),
label.Duration.Of(time.Since(startTime)),
)
if err != nil {
event.Error(ctx, eventName, err, lbls...)
} else {
event.Log(ctx, eventName, lbls...)
}
}
if standalone {
// Handle standalone package result.
//
// In general, this should just be a single "command-line-arguments"
// package containing the requested file. However, if the file is a test
// file, go/packages may return test variants of the command-line-arguments
// package. We don't support this; theoretically we could, but it seems
// unnecessarily complicated.
//
// Prior to golang/go#64233 we just assumed that we'd get exactly one
// package here. The categorization of bug reports below may be a bit
// verbose, but anticipates that perhaps we don't fully understand
// possible failure modes.
errorf := bug.Errorf
if s.view.typ == GoPackagesDriverView {
errorf = fmt.Errorf // all bets are off
}
var standalonePkg *packages.Package
for _, pkg := range pkgs {
if pkg.ID == "command-line-arguments" {
if standalonePkg != nil {
return errorf("internal error: go/packages returned multiple standalone packages")
}
standalonePkg = pkg
} else if packagesinternal.GetForTest(pkg) == "" && !strings.HasSuffix(pkg.ID, ".test") {
return errorf("internal error: go/packages returned unexpected package %q for standalone file", pkg.ID)
}
}
if standalonePkg == nil {
return errorf("internal error: go/packages failed to return non-test standalone package")
}
if len(standalonePkg.CompiledGoFiles) > 0 {
pkgs = []*packages.Package{standalonePkg}
} else {
pkgs = nil
}
}
if len(pkgs) == 0 {
if err == nil {
err = errNoPackages
}
return fmt.Errorf("packages.Load error: %w", err)
}
moduleErrs := make(map[string][]packages.Error) // module path -> errors
filterFunc := s.view.filterFunc()
newMetadata := make(map[PackageID]*metadata.Package)
for _, pkg := range pkgs {
if pkg.Module != nil && strings.Contains(pkg.Module.Path, "command-line-arguments") {
// golang/go#61543: modules containing "command-line-arguments" cause
// gopls to get all sorts of confused, because anything containing the
// string "command-line-arguments" is treated as a script. And yes, this
// happened in practice! (https://xkcd.com/327). Rather than try to work
// around this very rare edge case, just fail loudly.
return fmt.Errorf(`load failed: module name in %s contains "command-line-arguments", which is disallowed`, pkg.Module.GoMod)
}
// The Go command returns synthetic list results for module queries that
// encountered module errors.
//
// For example, given a module path a.mod, we'll query for "a.mod/..." and
// the go command will return a package named "a.mod/..." holding this
// error. Save it for later interpretation.
//
// See golang/go#50862 for more details.
if mod := moduleQueries[pkg.PkgPath]; mod != "" { // a synthetic result for the unloadable module
if len(pkg.Errors) > 0 {
moduleErrs[mod] = pkg.Errors
}
continue
}
if s.Options().VerboseOutput {
event.Log(ctx, eventName, append(
s.Labels(),
label.Package.Of(pkg.ID),
label.Files.Of(pkg.CompiledGoFiles))...)
}
// Ignore packages with no sources, since we will never be able to
// correctly invalidate that metadata.
if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 {
continue
}
// Special case for the builtin package, as it has no dependencies.
if pkg.PkgPath == "builtin" {
if len(pkg.GoFiles) != 1 {
return fmt.Errorf("only expected 1 file for builtin, got %v", len(pkg.GoFiles))
}
s.setBuiltin(pkg.GoFiles[0])
continue
}
// Skip test main packages.
if isTestMain(pkg, s.view.folder.Env.GOCACHE) {
continue
}
// Skip filtered packages. They may be added anyway if they're
// dependencies of non-filtered packages.
//
// TODO(rfindley): why exclude metadata arbitrarily here? It should be safe
// to capture all metadata.
// TODO(rfindley): what about compiled go files?
if allFilesExcluded(pkg.GoFiles, filterFunc) {
continue
}
buildMetadata(newMetadata, pkg, cfg.Dir, standalone, s.view.typ != GoPackagesDriverView)
}
s.mu.Lock()
// Assert the invariant s.packages.Get(id).m == s.meta.metadata[id].
s.packages.Range(func(id PackageID, ph *packageHandle) {
if s.meta.Packages[id] != ph.mp {
panic("inconsistent metadata")
}
})
// Compute the minimal metadata updates (for Clone)
// required to preserve the above invariant.
var files []protocol.DocumentURI // files to preload
seenFiles := make(map[protocol.DocumentURI]bool)
updates := make(map[PackageID]*metadata.Package)
for _, mp := range newMetadata {
if existing := s.meta.Packages[mp.ID]; existing == nil {
// Record any new files we should pre-load.
for _, uri := range mp.CompiledGoFiles {
if !seenFiles[uri] {
seenFiles[uri] = true
files = append(files, uri)
}
}
updates[mp.ID] = mp
s.shouldLoad.Delete(mp.ID)
}
}
if s.Options().VerboseOutput {
event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates)))
}
meta := s.meta.Update(updates)
workspacePackages := computeWorkspacePackagesLocked(ctx, s, meta)
s.meta = meta
s.workspacePackages = workspacePackages
s.resetActivePackagesLocked()
s.mu.Unlock()
// Opt: preLoad files in parallel.
//
// Requesting files in batch optimizes the underlying filesystem reads.
// However, this is also currently necessary for correctness: populating all
// files in the snapshot is necessary for certain operations that rely on the
// completeness of the file map, e.g. computing the set of directories to
// watch.
//
// TODO(rfindley, golang/go#57558): determine the set of directories based on
// loaded packages, so that reading files here is not necessary for
// correctness.
s.preloadFiles(ctx, files)
if len(moduleErrs) > 0 {
return &moduleErrorMap{moduleErrs}
}
return nil
}
type moduleErrorMap struct {
errs map[string][]packages.Error // module path -> errors
}
func (m *moduleErrorMap) Error() string {
var paths []string // sort for stability
for path, errs := range m.errs {
if len(errs) > 0 { // should always be true, but be cautious
paths = append(paths, path)
}
}
sort.Strings(paths)
var buf bytes.Buffer
fmt.Fprintf(&buf, "%d modules have errors:\n", len(paths))
for _, path := range paths {
fmt.Fprintf(&buf, "\t%s:%s\n", path, m.errs[path][0].Msg)
}
return buf.String()
}
// buildMetadata populates the updates map with metadata updates to
// apply, based on the given pkg. It recurs through pkg.Imports to ensure that
// metadata exists for all dependencies.
//
// Returns the metadata.Package that was built (or which was already present in
// updates), or nil if the package could not be built. Notably, the resulting
// metadata.Package may have an ID that differs from pkg.ID.
func buildMetadata(updates map[PackageID]*metadata.Package, pkg *packages.Package, loadDir string, standalone, goListView bool) *metadata.Package {
// Allow for multiple ad-hoc packages in the workspace (see #47584).
pkgPath := PackagePath(pkg.PkgPath)
id := PackageID(pkg.ID)
if metadata.IsCommandLineArguments(id) {
var f string // file to use as disambiguating suffix
if len(pkg.CompiledGoFiles) > 0 {
f = pkg.CompiledGoFiles[0]
// If there are multiple files,
// we can't use only the first.
// (Can this happen? #64557)
if len(pkg.CompiledGoFiles) > 1 {
bug.Reportf("unexpected files in command-line-arguments package: %v", pkg.CompiledGoFiles)
return nil
}
} else if len(pkg.IgnoredFiles) > 0 {
// A file=empty.go query results in IgnoredFiles=[empty.go].
f = pkg.IgnoredFiles[0]
} else {
bug.Reportf("command-line-arguments package has neither CompiledGoFiles nor IgnoredFiles")
return nil
}
id = PackageID(pkg.ID + f)
pkgPath = PackagePath(pkg.PkgPath + f)
}
// Duplicate?
if existing, ok := updates[id]; ok {
// A package was encountered twice due to shared
// subgraphs (common) or cycles (rare). Although "go
// list" usually breaks cycles, we don't rely on it.
// breakImportCycles in metadataGraph.Clone takes care
// of it later.
return existing
}
if pkg.TypesSizes == nil {
panic(id + ".TypeSizes is nil")
}
// Recreate the metadata rather than reusing it to avoid locking.
mp := &metadata.Package{
ID: id,
PkgPath: pkgPath,
Name: PackageName(pkg.Name),
ForTest: PackagePath(packagesinternal.GetForTest(pkg)),
TypesSizes: pkg.TypesSizes,
LoadDir: loadDir,
Module: pkg.Module,
Errors: pkg.Errors,
DepsErrors: packagesinternal.GetDepsErrors(pkg),
Standalone: standalone,
}
updates[id] = mp
for _, filename := range pkg.CompiledGoFiles {
uri := protocol.URIFromPath(filename)
mp.CompiledGoFiles = append(mp.CompiledGoFiles, uri)
}
for _, filename := range pkg.GoFiles {
uri := protocol.URIFromPath(filename)
mp.GoFiles = append(mp.GoFiles, uri)
}
for _, filename := range pkg.IgnoredFiles {
uri := protocol.URIFromPath(filename)
mp.IgnoredFiles = append(mp.IgnoredFiles, uri)
}
depsByImpPath := make(map[ImportPath]PackageID)
depsByPkgPath := make(map[PackagePath]PackageID)
for importPath, imported := range pkg.Imports {
importPath := ImportPath(importPath)
// It is not an invariant that importPath == imported.PkgPath.
// For example, package "net" imports "golang.org/x/net/dns/dnsmessage"
// which refers to the package whose ID and PkgPath are both
// "vendor/golang.org/x/net/dns/dnsmessage". Notice the ImportMap,
// which maps ImportPaths to PackagePaths:
//
// $ go list -json net vendor/golang.org/x/net/dns/dnsmessage
// {
// "ImportPath": "net",
// "Name": "net",
// "Imports": [
// "C",
// "vendor/golang.org/x/net/dns/dnsmessage",
// "vendor/golang.org/x/net/route",
// ...
// ],
// "ImportMap": {
// "golang.org/x/net/dns/dnsmessage": "vendor/golang.org/x/net/dns/dnsmessage",
// "golang.org/x/net/route": "vendor/golang.org/x/net/route"
// },
// ...
// }
// {
// "ImportPath": "vendor/golang.org/x/net/dns/dnsmessage",
// "Name": "dnsmessage",
// ...
// }
//
// (Beware that, for historical reasons, go list uses
// the JSON field "ImportPath" for the package's
// path--effectively the linker symbol prefix.)
//
// The example above is slightly special to go list
// because it's in the std module. Otherwise,
// vendored modules are simply modules whose directory
// is vendor/ instead of GOMODCACHE, and the
// import path equals the package path.
//
// But in GOPATH (non-module) mode, it's possible for
// package vendoring to cause a non-identity ImportMap,
// as in this example:
//
// $ cd $HOME/src
// $ find . -type f
// ./b/b.go
// ./vendor/example.com/a/a.go
// $ cat ./b/b.go
// package b
// import _ "example.com/a"
// $ cat ./vendor/example.com/a/a.go
// package a
// $ GOPATH=$HOME GO111MODULE=off go list -json ./b | grep -A2 ImportMap
// "ImportMap": {
// "example.com/a": "vendor/example.com/a"
// },
// Don't remember any imports with significant errors.
//
// The len=0 condition is a heuristic check for imports of
// non-existent packages (for which go/packages will create
// an edge to a synthesized node). The heuristic is unsound
// because some valid packages have zero files, for example,
// a directory containing only the file p_test.go defines an
// empty package p.
// TODO(adonovan): clarify this. Perhaps go/packages should
// report which nodes were synthesized.
if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 {
depsByImpPath[importPath] = "" // missing
continue
}
// Don't record self-import edges.
// (This simplifies metadataGraph's cycle check.)
if PackageID(imported.ID) == id {
if len(pkg.Errors) == 0 {
bug.Reportf("self-import without error in package %s", id)
}
continue
}
dep := buildMetadata(updates, imported, loadDir, false, goListView) // only top level packages can be standalone
// Don't record edges to packages with no name, as they cause trouble for
// the importer (golang/go#60952).
//
// Also don't record edges to packages whose ID was modified (i.e.
// command-line-arguments packages), as encountered in golang/go#66109. In
// this case, we could theoretically keep the edge through dep.ID, but
// since this import doesn't make any sense in the first place, we instead
// choose to consider it invalid.
//
// However, we do want to insert these packages into the update map
// (buildMetadata above), so that we get type-checking diagnostics for the
// invalid packages.
if dep == nil || dep.ID != PackageID(imported.ID) || imported.Name == "" {
depsByImpPath[importPath] = "" // missing
continue
}
depsByImpPath[importPath] = PackageID(imported.ID)
depsByPkgPath[PackagePath(imported.PkgPath)] = PackageID(imported.ID)
}
mp.DepsByImpPath = depsByImpPath
mp.DepsByPkgPath = depsByPkgPath
return mp
// m.Diagnostics is set later in the loading pass, using
// computeLoadDiagnostics.
}
// computeLoadDiagnostics computes and sets m.Diagnostics for the given metadata m.
//
// It should only be called during package handle construction in buildPackageHandle.
func computeLoadDiagnostics(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) []*Diagnostic {
var diags []*Diagnostic
for _, packagesErr := range mp.Errors {
// Filter out parse errors from go list. We'll get them when we
// actually parse, and buggy overlay support may generate spurious
// errors. (See TestNewModule_Issue38207.)
if strings.Contains(packagesErr.Msg, "expected '") {
continue
}
pkgDiags, err := goPackagesErrorDiagnostics(ctx, packagesErr, mp, snapshot)
if err != nil {
// There are certain cases where the go command returns invalid
// positions, so we cannot panic or even bug.Reportf here.
event.Error(ctx, "unable to compute positions for list errors", err, label.Package.Of(string(mp.ID)))
continue
}
diags = append(diags, pkgDiags...)
}
// TODO(rfindley): this is buggy: an insignificant change to a modfile
// (or an unsaved modfile) could affect the position of deps errors,
// without invalidating the package.
depsDiags, err := depsErrors(ctx, snapshot, mp)
if err != nil {
if ctx.Err() == nil {
// TODO(rfindley): consider making this a bug.Reportf. depsErrors should
// not normally fail.
event.Error(ctx, "unable to compute deps errors", err, label.Package.Of(string(mp.ID)))
}
} else {
diags = append(diags, depsDiags...)
}
return diags
}
// IsWorkspacePackage reports whether id points to a workspace package in s.
//
// Currently, the result depends on the current set of loaded packages, and so
// is not guaranteed to be stable.
func (s *Snapshot) IsWorkspacePackage(ctx context.Context, id PackageID) bool {
s.mu.Lock()
defer s.mu.Unlock()
mg := s.meta
m := mg.Packages[id]
if m == nil {
return false
}
return isWorkspacePackageLocked(ctx, s, mg, m)
}
// isWorkspacePackageLocked reports whether p is a workspace package for the
// snapshot s.
//
// Workspace packages are packages that we consider the user to be actively
// working on. As such, they are re-diagnosed on every keystroke, and searched
// for various workspace-wide queries such as references or workspace symbols.
//
// See the commentary inline for a description of the workspace package
// heuristics.
//
// s.mu must be held while calling this function.
//
// TODO(rfindley): remove 'meta' from this function signature. Whether or not a
// package is a workspace package should depend only on the package, view
// definition, and snapshot file source. While useful, the heuristic
// "allFilesHaveRealPackages" does not add that much value and is path
// dependent as it depends on the timing of loads.
func isWorkspacePackageLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph, pkg *metadata.Package) bool {
if metadata.IsCommandLineArguments(pkg.ID) {
// Ad-hoc command-line-arguments packages aren't workspace packages.
// With zero-config gopls (golang/go#57979) they should be very rare, as
// they should only arise when the user opens a file outside the workspace
// which isn't present in the import graph of a workspace package.
//
// Considering them as workspace packages tends to be racy, as they don't
// deterministically belong to any view.
if !pkg.Standalone {
return false
}
// If all the files contained in pkg have a real package, we don't need to
// keep pkg as a workspace package.
if allFilesHaveRealPackages(meta, pkg) {
return false
}
// For now, allow open standalone packages (i.e. go:build ignore) to be
// workspace packages, but this means they could belong to multiple views.
return containsOpenFileLocked(s, pkg)
}
// If a real package is open, consider it to be part of the workspace.
//
// TODO(rfindley): reconsider this. In golang/go#66145, we saw that even if a
// View sees a real package for a file, it doesn't mean that View is able to
// cleanly diagnose the package. Yet, we do want to show diagnostics for open
// packages outside the workspace. Is there a better way to ensure that only
// the 'best' View gets a workspace package for the open file?
if containsOpenFileLocked(s, pkg) {
return true
}
// Apply filtering logic.
//
// Workspace packages must contain at least one non-filtered file.
filterFunc := s.view.filterFunc()
uris := make(map[protocol.DocumentURI]unit) // filtered package URIs
for _, uri := range slices.Concat(pkg.CompiledGoFiles, pkg.GoFiles) {
if !strings.Contains(string(uri), "/vendor/") && !filterFunc(uri) {
uris[uri] = struct{}{}
}
}
if len(uris) == 0 {
return false // no non-filtered files
}
// For non-module views (of type GOPATH or AdHoc), or if
// expandWorkspaceToModule is unset, workspace packages must be contained in
// the workspace folder.
//
// For module views (of type GoMod or GoWork), packages must in any case be
// in a workspace module (enforced below).
if !s.view.typ.usesModules() || !s.Options().ExpandWorkspaceToModule {
folder := s.view.folder.Dir.Path()
inFolder := false
for uri := range uris {
if pathutil.InDir(folder, uri.Path()) {
inFolder = true
break
}
}
if !inFolder {
return false
}
}
// In module mode, a workspace package must be contained in a workspace
// module.
if s.view.typ.usesModules() {
var modURI protocol.DocumentURI
if pkg.Module != nil {
modURI = protocol.URIFromPath(pkg.Module.GoMod)
} else {
// golang/go#65816: for std and cmd, Module is nil.
// Fall back to an inferior heuristic.
if len(pkg.CompiledGoFiles) == 0 {
return false // need at least one file to guess the go.mod file
}
dir := pkg.CompiledGoFiles[0].Dir()
var err error
modURI, err = findRootPattern(ctx, dir, "go.mod", lockedSnapshot{s})
if err != nil || modURI == "" {
// err != nil implies context cancellation, in which case the result of
// this query does not matter.
return false
}
}
_, ok := s.view.workspaceModFiles[modURI]
return ok
}
return true // an ad-hoc package or GOPATH package
}
// containsOpenFileLocked reports whether any file referenced by m is open in
// the snapshot s.
//
// s.mu must be held while calling this function.
func containsOpenFileLocked(s *Snapshot, mp *metadata.Package) bool {
uris := map[protocol.DocumentURI]struct{}{}
for _, uri := range mp.CompiledGoFiles {
uris[uri] = struct{}{}
}
for _, uri := range mp.GoFiles {
uris[uri] = struct{}{}
}
for uri := range uris {
fh, _ := s.files.get(uri)
if _, open := fh.(*overlay); open {
return true
}
}
return false
}
// computeWorkspacePackagesLocked computes workspace packages in the
// snapshot s for the given metadata graph. The result does not
// contain intermediate test variants.
//
// s.mu must be held while calling this function.
func computeWorkspacePackagesLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph) immutable.Map[PackageID, PackagePath] {
// The provided context is used for reading snapshot files, which can only
// fail due to context cancellation. Don't let this happen as it could lead
// to inconsistent results.
ctx = xcontext.Detach(ctx)
workspacePackages := make(map[PackageID]PackagePath)
for _, mp := range meta.Packages {
if !isWorkspacePackageLocked(ctx, s, meta, mp) {
continue
}
switch {
case mp.ForTest == "":
// A normal package.
workspacePackages[mp.ID] = mp.PkgPath
case mp.ForTest == mp.PkgPath, mp.ForTest+"_test" == mp.PkgPath:
// The test variant of some workspace package or its x_test.
// To load it, we need to load the non-test variant with -test.
//
// Notably, this excludes intermediate test variants from workspace
// packages.
assert(!mp.IsIntermediateTestVariant(), "unexpected ITV")
workspacePackages[mp.ID] = mp.ForTest
}
}
return immutable.MapOf(workspacePackages)
}
// allFilesHaveRealPackages reports whether all files referenced by m are
// contained in a "real" package (not command-line-arguments).
//
// If m is valid but all "real" packages containing any file are invalid, this
// function returns false.
//
// If m is not a command-line-arguments package, this is trivially true.
func allFilesHaveRealPackages(g *metadata.Graph, mp *metadata.Package) bool {
n := len(mp.CompiledGoFiles)
checkURIs:
for _, uri := range append(mp.CompiledGoFiles[0:n:n], mp.GoFiles...) {
for _, id := range g.IDs[uri] {
if !metadata.IsCommandLineArguments(id) {
continue checkURIs
}
}
return false
}
return true
}
func isTestMain(pkg *packages.Package, gocache string) bool {
// Test mains must have an import path that ends with ".test".
if !strings.HasSuffix(pkg.PkgPath, ".test") {
return false
}
// Test main packages are always named "main".
if pkg.Name != "main" {
return false
}
// Test mains always have exactly one GoFile that is in the build cache.
if len(pkg.GoFiles) > 1 {
return false
}
if !pathutil.InDir(gocache, pkg.GoFiles[0]) {
return false
}
return true
}
|