Skip to content

Commit b253314

Browse files
committed
gopls/internal/lsp/cache: add support for loading standalone main files
Add support in gopls for working on "standalone main files", which are Go source files that should be treated as standalone packages. Standalone files are identified by a specific build tag, which may be configured via the new standaloneTags setting. For example, it is common to use the directive "//go:build ignore" to colocate standalone files with other package files. Specifically, - add a new loadScope interface for use in snapshot.load, to add a bit of type safety - add a new standaloneTags setting to allow configuring the set of build constraints that define standalone main files - add an isStandaloneFile function that detects standalone files based on build constraints - implement the loading of standalone files, by querying go/packages for the standalone file path - rewrite getOrLoadIDsForURI, which had inconsistent behavior with respect to error handling and the experimentalUseInvalidMetadata setting - update the WorkspaceSymbols handler to properly format command-line-arguments packages - add regression tests for LSP behavior with standalone files, and for dynamic configuration of standalone files Fixes golang/go#49657 Change-Id: I7b79257a984a87b67e476c32dec3c122f9bbc636 Reviewed-on: https://go-review.googlesource.com/c/tools/+/441877 gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 3beecff commit b253314

18 files changed

+656
-67
lines changed

gopls/doc/settings.md

+23
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,29 @@ version of gopls (https://go.dev/issue/55333).
174174

175175
Default: `false`.
176176

177+
#### **standaloneTags** *[]string*
178+
179+
standaloneTags specifies a set of build constraints that identify
180+
individual Go source files that make up the entire main package of an
181+
executable.
182+
183+
A common example of standalone main files is the convention of using the
184+
directive `//go:build ignore` to denote files that are not intended to be
185+
included in any package, for example because they are invoked directly by
186+
the developer using `go run`.
187+
188+
Gopls considers a file to be a standalone main file if and only if it has
189+
package name "main" and has a build directive of the exact form
190+
"//go:build tag" or "// +build tag", where tag is among the list of tags
191+
configured by this setting. Notably, if the build constraint is more
192+
complicated than a simple tag (such as the composite constraint
193+
`//go:build tag && go1.18`), the file is not considered to be a standalone
194+
main file.
195+
196+
This setting is only supported when gopls is built with Go 1.16 or later.
197+
198+
Default: `["ignore"]`.
199+
177200
### Formatting
178201

179202
#### **local** *string*

gopls/internal/lsp/cache/load.go

+23-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package cache
77
import (
88
"bytes"
99
"context"
10+
"errors"
1011
"fmt"
1112
"io/ioutil"
1213
"os"
@@ -28,12 +29,15 @@ import (
2829

2930
var loadID uint64 // atomic identifier for loads
3031

32+
// errNoPackages indicates that a load query matched no packages.
33+
var errNoPackages = errors.New("no packages returned")
34+
3135
// load calls packages.Load for the given scopes, updating package metadata,
3236
// import graph, and mapped files with the result.
3337
//
3438
// The resulting error may wrap the moduleErrorMap error type, representing
3539
// errors associated with specific modules.
36-
func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) {
40+
func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) {
3741
id := atomic.AddUint64(&loadID, 1)
3842
eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging
3943

@@ -45,22 +49,32 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
4549
moduleQueries := make(map[string]string)
4650
for _, scope := range scopes {
4751
switch scope := scope.(type) {
48-
case PackagePath:
52+
case packageLoadScope:
4953
if source.IsCommandLineArguments(string(scope)) {
5054
panic("attempted to load command-line-arguments")
5155
}
5256
// The only time we pass package paths is when we're doing a
5357
// partial workspace load. In those cases, the paths came back from
5458
// go list and should already be GOPATH-vendorized when appropriate.
5559
query = append(query, string(scope))
56-
case fileURI:
60+
61+
case fileLoadScope:
5762
uri := span.URI(scope)
58-
// Don't try to load a file that doesn't exist.
5963
fh := s.FindFile(uri)
6064
if fh == nil || s.View().FileKind(fh) != source.Go {
65+
// Don't try to load a file that doesn't exist, or isn't a go file.
6166
continue
6267
}
63-
query = append(query, fmt.Sprintf("file=%s", uri.Filename()))
68+
contents, err := fh.Read()
69+
if err != nil {
70+
continue
71+
}
72+
if isStandaloneFile(contents, s.view.Options().StandaloneTags) {
73+
query = append(query, uri.Filename())
74+
} else {
75+
query = append(query, fmt.Sprintf("file=%s", uri.Filename()))
76+
}
77+
6478
case moduleLoadScope:
6579
switch scope {
6680
case "std", "cmd":
@@ -70,6 +84,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
7084
query = append(query, modQuery)
7185
moduleQueries[modQuery] = string(scope)
7286
}
87+
7388
case viewLoadScope:
7489
// If we are outside of GOPATH, a module, or some other known
7590
// build system, don't load subdirectories.
@@ -78,6 +93,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
7893
} else {
7994
query = append(query, "./...")
8095
}
96+
8197
default:
8298
panic(fmt.Sprintf("unknown scope type %T", scope))
8399
}
@@ -136,9 +152,9 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
136152

137153
if len(pkgs) == 0 {
138154
if err == nil {
139-
err = fmt.Errorf("no packages returned")
155+
err = errNoPackages
140156
}
141-
return fmt.Errorf("%v: %w", err, source.PackagesLoadError)
157+
return fmt.Errorf("packages.Load error: %w", err)
142158
}
143159

144160
moduleErrs := make(map[string][]packages.Error) // module path -> errors

gopls/internal/lsp/cache/metadata.go

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ type Metadata struct {
4141
Config *packages.Config
4242
}
4343

44+
// PackageID implements the source.Metadata interface.
45+
func (m *Metadata) PackageID() string {
46+
return string(m.ID)
47+
}
48+
4449
// Name implements the source.Metadata interface.
4550
func (m *Metadata) PackageName() string {
4651
return string(m.Name)

gopls/internal/lsp/cache/pkg.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,24 @@ type pkg struct {
3232
hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors
3333
}
3434

35-
// Declare explicit types for files and directories to distinguish between the two.
35+
// A loadScope defines a package loading scope for use with go/packages.
36+
type loadScope interface {
37+
aScope()
38+
}
39+
3640
type (
37-
fileURI span.URI
38-
moduleLoadScope string
39-
viewLoadScope span.URI
41+
fileLoadScope span.URI // load packages containing a file (including command-line-arguments)
42+
packageLoadScope string // load a specific package
43+
moduleLoadScope string // load packages in a specific module
44+
viewLoadScope span.URI // load the workspace
4045
)
4146

47+
// Implement the loadScope interface.
48+
func (fileLoadScope) aScope() {}
49+
func (packageLoadScope) aScope() {}
50+
func (moduleLoadScope) aScope() {}
51+
func (viewLoadScope) aScope() {}
52+
4253
func (p *pkg) ID() string {
4354
return string(p.m.ID)
4455
}

gopls/internal/lsp/cache/snapshot.go

+82-37
Original file line numberDiff line numberDiff line change
@@ -712,51 +712,94 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode
712712
return phs, nil
713713
}
714714

715+
// getOrLoadIDsForURI returns package IDs associated with the file uri. If no
716+
// such packages exist or if they are known to be stale, it reloads the file.
717+
//
718+
// If experimentalUseInvalidMetadata is set, this function may return package
719+
// IDs with invalid metadata.
715720
func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]PackageID, error) {
721+
useInvalidMetadata := s.useInvalidMetadata()
722+
716723
s.mu.Lock()
724+
725+
// Start with the set of package associations derived from the last load.
717726
ids := s.meta.ids[uri]
718-
reload := len(ids) == 0
727+
728+
hasValidID := false // whether we have any valid package metadata containing uri
729+
shouldLoad := false // whether any packages containing uri are marked 'shouldLoad'
719730
for _, id := range ids {
720-
// If the file is part of a package that needs reloading, reload it now to
721-
// improve our responsiveness.
731+
// TODO(rfindley): remove the defensiveness here. s.meta.metadata[id] must
732+
// exist.
733+
if m, ok := s.meta.metadata[id]; ok && m.Valid {
734+
hasValidID = true
735+
}
722736
if len(s.shouldLoad[id]) > 0 {
723-
reload = true
724-
break
737+
shouldLoad = true
725738
}
726-
// TODO(golang/go#36918): Previously, we would reload any package with
727-
// missing dependencies. This is expensive and results in too many
728-
// calls to packages.Load. Determine what we should do instead.
729739
}
740+
741+
// Check if uri is known to be unloadable.
742+
//
743+
// TODO(rfindley): shouldn't we also mark uri as unloadable if the load below
744+
// fails? Otherwise we endlessly load files with no packages.
745+
_, unloadable := s.unloadableFiles[uri]
746+
730747
s.mu.Unlock()
731748

732-
if reload {
733-
scope := fileURI(uri)
749+
// Special case: if experimentalUseInvalidMetadata is set and we have any
750+
// ids, just return them.
751+
//
752+
// This is arguably wrong: if the metadata is invalid we should try reloading
753+
// it. However, this was the pre-existing behavior, and
754+
// experimentalUseInvalidMetadata will be removed in a future release.
755+
if !shouldLoad && useInvalidMetadata && len(ids) > 0 {
756+
return ids, nil
757+
}
758+
759+
// Reload if loading is likely to improve the package associations for uri:
760+
// - uri is not contained in any valid packages
761+
// - ...or one of the packages containing uri is marked 'shouldLoad'
762+
// - ...but uri is not unloadable
763+
if (shouldLoad || !hasValidID) && !unloadable {
764+
scope := fileLoadScope(uri)
734765
err := s.load(ctx, false, scope)
735766

736-
// As in reloadWorkspace, we must clear scopes after loading.
767+
// Guard against failed loads due to context cancellation.
737768
//
738-
// TODO(rfindley): simply call reloadWorkspace here, first, to avoid this
739-
// duplication.
740-
if !errors.Is(err, context.Canceled) {
741-
s.clearShouldLoad(scope)
769+
// Return the context error here as the current operation is no longer
770+
// valid.
771+
if ctxErr := ctx.Err(); ctxErr != nil {
772+
return nil, ctxErr
742773
}
743774

744-
// TODO(rfindley): this doesn't look right. If we don't reload, we use
745-
// invalid metadata anyway, but if we DO reload and it fails, we don't?
746-
if !s.useInvalidMetadata() && err != nil {
747-
return nil, err
748-
}
775+
// We must clear scopes after loading.
776+
//
777+
// TODO(rfindley): unlike reloadWorkspace, this is simply marking loaded
778+
// packages as loaded. We could do this from snapshot.load and avoid
779+
// raciness.
780+
s.clearShouldLoad(scope)
749781

750-
s.mu.Lock()
751-
ids = s.meta.ids[uri]
752-
s.mu.Unlock()
782+
// Don't return an error here, as we may still return stale IDs.
783+
// Furthermore, the result of getOrLoadIDsForURI should be consistent upon
784+
// subsequent calls, even if the file is marked as unloadable.
785+
if err != nil && !errors.Is(err, errNoPackages) {
786+
event.Error(ctx, "getOrLoadIDsForURI", err)
787+
}
788+
}
753789

754-
// We've tried to reload and there are still no known IDs for the URI.
755-
// Return the load error, if there was one.
756-
if len(ids) == 0 {
757-
return nil, err
790+
s.mu.Lock()
791+
ids = s.meta.ids[uri]
792+
if !useInvalidMetadata {
793+
var validIDs []PackageID
794+
for _, id := range ids {
795+
// TODO(rfindley): remove the defensiveness here as well.
796+
if m, ok := s.meta.metadata[id]; ok && m.Valid {
797+
validIDs = append(validIDs, id)
798+
}
758799
}
800+
ids = validIDs
759801
}
802+
s.mu.Unlock()
760803

761804
return ids, nil
762805
}
@@ -1206,25 +1249,26 @@ func (s *snapshot) getMetadata(id PackageID) *KnownMetadata {
12061249

12071250
// clearShouldLoad clears package IDs that no longer need to be reloaded after
12081251
// scopes has been loaded.
1209-
func (s *snapshot) clearShouldLoad(scopes ...interface{}) {
1252+
func (s *snapshot) clearShouldLoad(scopes ...loadScope) {
12101253
s.mu.Lock()
12111254
defer s.mu.Unlock()
12121255

12131256
for _, scope := range scopes {
12141257
switch scope := scope.(type) {
1215-
case PackagePath:
1258+
case packageLoadScope:
1259+
scopePath := PackagePath(scope)
12161260
var toDelete []PackageID
12171261
for id, pkgPaths := range s.shouldLoad {
12181262
for _, pkgPath := range pkgPaths {
1219-
if pkgPath == scope {
1263+
if pkgPath == scopePath {
12201264
toDelete = append(toDelete, id)
12211265
}
12221266
}
12231267
}
12241268
for _, id := range toDelete {
12251269
delete(s.shouldLoad, id)
12261270
}
1227-
case fileURI:
1271+
case fileLoadScope:
12281272
uri := span.URI(scope)
12291273
ids := s.meta.ids[uri]
12301274
for _, id := range ids {
@@ -1481,7 +1525,7 @@ func (s *snapshot) AwaitInitialized(ctx context.Context) {
14811525

14821526
// reloadWorkspace reloads the metadata for all invalidated workspace packages.
14831527
func (s *snapshot) reloadWorkspace(ctx context.Context) error {
1484-
var scopes []interface{}
1528+
var scopes []loadScope
14851529
var seen map[PackagePath]bool
14861530
s.mu.Lock()
14871531
for _, pkgPaths := range s.shouldLoad {
@@ -1493,7 +1537,7 @@ func (s *snapshot) reloadWorkspace(ctx context.Context) error {
14931537
continue
14941538
}
14951539
seen[pkgPath] = true
1496-
scopes = append(scopes, pkgPath)
1540+
scopes = append(scopes, packageLoadScope(pkgPath))
14971541
}
14981542
}
14991543
s.mu.Unlock()
@@ -1505,7 +1549,7 @@ func (s *snapshot) reloadWorkspace(ctx context.Context) error {
15051549
// If the view's build configuration is invalid, we cannot reload by
15061550
// package path. Just reload the directory instead.
15071551
if !s.ValidBuildConfiguration() {
1508-
scopes = []interface{}{viewLoadScope("LOAD_INVALID_VIEW")}
1552+
scopes = []loadScope{viewLoadScope("LOAD_INVALID_VIEW")}
15091553
}
15101554

15111555
err := s.load(ctx, false, scopes...)
@@ -1527,7 +1571,7 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error {
15271571
files := s.orphanedFiles()
15281572

15291573
// Files without a valid package declaration can't be loaded. Don't try.
1530-
var scopes []interface{}
1574+
var scopes []loadScope
15311575
for _, file := range files {
15321576
pgf, err := s.ParseGo(ctx, file, source.ParseHeader)
15331577
if err != nil {
@@ -1536,7 +1580,8 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error {
15361580
if !pgf.File.Package.IsValid() {
15371581
continue
15381582
}
1539-
scopes = append(scopes, fileURI(file.URI()))
1583+
1584+
scopes = append(scopes, fileLoadScope(file.URI()))
15401585
}
15411586

15421587
if len(scopes) == 0 {
@@ -1560,7 +1605,7 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error {
15601605
event.Error(ctx, "reloadOrphanedFiles: failed to load", err, tag.Query.Of(scopes))
15611606
s.mu.Lock()
15621607
for _, scope := range scopes {
1563-
uri := span.URI(scope.(fileURI))
1608+
uri := span.URI(scope.(fileLoadScope))
15641609
if s.noValidMetadataForURILocked(uri) {
15651610
s.unloadableFiles[uri] = struct{}{}
15661611
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2022 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+
//go:build !go1.16
6+
// +build !go1.16
7+
8+
package cache
9+
10+
// isStandaloneFile returns false, as the 'standaloneTags' setting is
11+
// unsupported on Go 1.15 and earlier.
12+
func isStandaloneFile(src []byte, standaloneTags []string) bool {
13+
return false
14+
}

0 commit comments

Comments
 (0)