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 <noreply+kokoro@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go
index e40ecc2..2b7362c 100755
--- a/gopls/internal/lsp/source/api_json.go
+++ b/gopls/internal/lsp/source/api_json.go
@@ -97,6 +97,13 @@
Hierarchy: "build",
},
{
+ Name: "standaloneTags",
+ Type: "[]string",
+ Doc: "standaloneTags specifies a set of build constraints that identify\nindividual Go source files that make up the entire main package of an\nexecutable.\n\nA common example of standalone main files is the convention of using the\ndirective `//go:build ignore` to denote files that are not intended to be\nincluded in any package, for example because they are invoked directly by\nthe developer using `go run`.\n\nGopls considers a file to be a standalone main file if and only if it has\npackage name \"main\" and has a build directive of the exact form\n\"//go:build tag\" or \"// +build tag\", where tag is among the list of tags\nconfigured by this setting. Notably, if the build constraint is more\ncomplicated than a simple tag (such as the composite constraint\n`//go:build tag && go1.18`), the file is not considered to be a standalone\nmain file.\n\nThis setting is only supported when gopls is built with Go 1.16 or later.\n",
+ Default: "[\"ignore\"]",
+ Hierarchy: "build",
+ },
+ {
Name: "hoverKind",
Type: "enum",
Doc: "hoverKind controls the information that appears in the hover text.\nSingleLine and Structured are intended for use only by authors of editor plugins.\n",
diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go
index 00c9b7b..fe86ff2 100644
--- a/gopls/internal/lsp/source/options.go
+++ b/gopls/internal/lsp/source/options.go
@@ -120,6 +120,7 @@
MemoryMode: ModeNormal,
DirectoryFilters: []string{"-**/node_modules"},
TemplateExtensions: []string{},
+ StandaloneTags: []string{"ignore"},
},
UIOptions: UIOptions{
DiagnosticOptions: DiagnosticOptions{
@@ -298,6 +299,26 @@
// Deprecated: this setting is deprecated and will be removed in a future
// version of gopls (https://go.dev/issue/55333).
ExperimentalUseInvalidMetadata bool `status:"experimental"`
+
+ // StandaloneTags specifies a set of build constraints that identify
+ // individual Go source files that make up the entire main package of an
+ // executable.
+ //
+ // A common example of standalone main files is the convention of using the
+ // directive `//go:build ignore` to denote files that are not intended to be
+ // included in any package, for example because they are invoked directly by
+ // the developer using `go run`.
+ //
+ // Gopls considers a file to be a standalone main file if and only if it has
+ // package name "main" and has a build directive of the exact form
+ // "//go:build tag" or "// +build tag", where tag is among the list of tags
+ // configured by this setting. Notably, if the build constraint is more
+ // complicated than a simple tag (such as the composite constraint
+ // `//go:build tag && go1.18`), the file is not considered to be a standalone
+ // main file.
+ //
+ // This setting is only supported when gopls is built with Go 1.16 or later.
+ StandaloneTags []string
}
type UIOptions struct {
@@ -760,6 +781,8 @@
}
func (o *Options) Clone() *Options {
+ // TODO(rfindley): has this function gone stale? It appears that there are
+ // settings that are incorrectly cloned here (such as TemplateExtensions).
result := &Options{
ClientOptions: o.ClientOptions,
InternalOptions: o.InternalOptions,
@@ -793,6 +816,7 @@
result.SetEnvSlice(o.EnvSlice())
result.BuildFlags = copySlice(o.BuildFlags)
result.DirectoryFilters = copySlice(o.DirectoryFilters)
+ result.StandaloneTags = copySlice(o.StandaloneTags)
copyAnalyzerMap := func(src map[string]*Analyzer) map[string]*Analyzer {
dst := make(map[string]*Analyzer)
@@ -887,6 +911,7 @@
}
case "buildFlags":
+ // TODO(rfindley): use asStringSlice.
iflags, ok := value.([]interface{})
if !ok {
result.parseErrorf("invalid type %T, expect list", value)
@@ -897,7 +922,9 @@
flags = append(flags, fmt.Sprintf("%s", flag))
}
o.BuildFlags = flags
+
case "directoryFilters":
+ // TODO(rfindley): use asStringSlice.
ifilters, ok := value.([]interface{})
if !ok {
result.parseErrorf("invalid type %T, expect list", value)
@@ -913,6 +940,7 @@
filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/"))
}
o.DirectoryFilters = filters
+
case "memoryMode":
if s, ok := result.asOneOf(
string(ModeNormal),
@@ -1104,6 +1132,9 @@
result.softErrorf(msg)
result.setBool(&o.ExperimentalUseInvalidMetadata)
+ case "standaloneTags":
+ result.setStringSlice(&o.StandaloneTags)
+
case "allExperiments":
// This setting should be handled before all of the other options are
// processed, so do nothing here.
@@ -1294,6 +1325,24 @@
return b, true
}
+func (r *OptionResult) asStringSlice() ([]string, bool) {
+ iList, ok := r.Value.([]interface{})
+ if !ok {
+ r.parseErrorf("invalid type %T, expect list", r.Value)
+ return nil, false
+ }
+ var list []string
+ for _, elem := range iList {
+ s, ok := elem.(string)
+ if !ok {
+ r.parseErrorf("invalid element type %T, expect string", elem)
+ return nil, false
+ }
+ list = append(list, s)
+ }
+ return list, true
+}
+
func (r *OptionResult) asOneOf(options ...string) (string, bool) {
s, ok := r.asString()
if !ok {
@@ -1322,6 +1371,12 @@
}
}
+func (r *OptionResult) setStringSlice(s *[]string) {
+ if v, ok := r.asStringSlice(); ok {
+ *s = v
+ }
+}
+
// EnabledAnalyzers returns all of the analyzers enabled for the given
// snapshot.
func EnabledAnalyzers(snapshot Snapshot) (analyzers []*Analyzer) {
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index e986d12..d242cf4 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -331,7 +331,12 @@
}
// Metadata represents package metadata retrieved from go/packages.
+//
+// TODO(rfindley): move the strongly typed strings from the cache package here.
type Metadata interface {
+ // PackageID is the unique package id.
+ PackageID() string
+
// PackageName is the package name.
PackageName() string
@@ -653,10 +658,6 @@
return DiagnosticSource(name)
}
-var (
- PackagesLoadError = errors.New("packages.Load error")
-)
-
// WorkspaceModuleVersion is the nonexistent pseudoversion suffix used in the
// construction of the workspace module. It is exported so that we can make
// sure not to show this version to end users in error messages, to avoid
diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go
index dabbeb3..bd1e7b1 100644
--- a/gopls/internal/lsp/source/workspace_symbol.go
+++ b/gopls/internal/lsp/source/workspace_symbol.go
@@ -98,6 +98,12 @@
}
func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) {
+ if IsCommandLineArguments(pkg.PackageID()) {
+ // command-line-arguments packages have a non-sensical package path, so
+ // just use their package name.
+ return packageSymbolMatch(space, name, pkg, matcher)
+ }
+
var score float64
endsInPkgName := strings.HasSuffix(pkg.PackagePath(), pkg.PackageName())