// 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 source import ( "context" "fmt" "io" "path/filepath" "regexp" "runtime" "strings" "sync" "time" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/asmdecl" "golang.org/x/tools/go/analysis/passes/assign" "golang.org/x/tools/go/analysis/passes/atomic" "golang.org/x/tools/go/analysis/passes/atomicalign" "golang.org/x/tools/go/analysis/passes/bools" "golang.org/x/tools/go/analysis/passes/buildtag" "golang.org/x/tools/go/analysis/passes/cgocall" "golang.org/x/tools/go/analysis/passes/composite" "golang.org/x/tools/go/analysis/passes/copylock" "golang.org/x/tools/go/analysis/passes/deepequalerrors" "golang.org/x/tools/go/analysis/passes/errorsas" "golang.org/x/tools/go/analysis/passes/fieldalignment" "golang.org/x/tools/go/analysis/passes/httpresponse" "golang.org/x/tools/go/analysis/passes/ifaceassert" "golang.org/x/tools/go/analysis/passes/loopclosure" "golang.org/x/tools/go/analysis/passes/lostcancel" "golang.org/x/tools/go/analysis/passes/nilfunc" "golang.org/x/tools/go/analysis/passes/nilness" "golang.org/x/tools/go/analysis/passes/printf" "golang.org/x/tools/go/analysis/passes/shadow" "golang.org/x/tools/go/analysis/passes/shift" "golang.org/x/tools/go/analysis/passes/sortslice" "golang.org/x/tools/go/analysis/passes/stdmethods" "golang.org/x/tools/go/analysis/passes/stringintconv" "golang.org/x/tools/go/analysis/passes/structtag" "golang.org/x/tools/go/analysis/passes/testinggoroutine" "golang.org/x/tools/go/analysis/passes/tests" "golang.org/x/tools/go/analysis/passes/unmarshal" "golang.org/x/tools/go/analysis/passes/unreachable" "golang.org/x/tools/go/analysis/passes/unsafeptr" "golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/go/analysis/passes/unusedwrite" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/lsp/analysis/embeddirective" "golang.org/x/tools/internal/lsp/analysis/fillreturns" "golang.org/x/tools/internal/lsp/analysis/fillstruct" "golang.org/x/tools/internal/lsp/analysis/infertypeargs" "golang.org/x/tools/internal/lsp/analysis/nonewvars" "golang.org/x/tools/internal/lsp/analysis/noresultvalues" "golang.org/x/tools/internal/lsp/analysis/simplifycompositelit" "golang.org/x/tools/internal/lsp/analysis/simplifyrange" "golang.org/x/tools/internal/lsp/analysis/simplifyslice" "golang.org/x/tools/internal/lsp/analysis/stubmethods" "golang.org/x/tools/internal/lsp/analysis/undeclaredname" "golang.org/x/tools/internal/lsp/analysis/unusedparams" "golang.org/x/tools/internal/lsp/analysis/useany" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/lsp/diff/myers" "golang.org/x/tools/internal/lsp/protocol" ) var ( optionsOnce sync.Once defaultOptions *Options ) // DefaultOptions is the options that are used for Gopls execution independent // of any externally provided configuration (LSP initialization, command // invocation, etc.). func DefaultOptions() *Options { optionsOnce.Do(func() { var commands []string for _, c := range command.Commands { commands = append(commands, c.ID()) } defaultOptions = &Options{ ClientOptions: ClientOptions{ InsertTextFormat: protocol.PlainTextTextFormat, PreferredContentFormat: protocol.Markdown, ConfigurationSupported: true, DynamicConfigurationSupported: true, DynamicRegistrationSemanticTokensSupported: true, DynamicWatchedFilesSupported: true, LineFoldingOnly: false, HierarchicalDocumentSymbolSupport: true, }, ServerOptions: ServerOptions{ SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{ Go: { protocol.SourceFixAll: true, protocol.SourceOrganizeImports: true, protocol.QuickFix: true, protocol.RefactorRewrite: true, protocol.RefactorExtract: true, }, Mod: { protocol.SourceOrganizeImports: true, protocol.QuickFix: true, }, Work: {}, Sum: {}, Tmpl: {}, }, SupportedCommands: commands, }, UserOptions: UserOptions{ BuildOptions: BuildOptions{ ExpandWorkspaceToModule: true, ExperimentalPackageCacheKey: true, MemoryMode: ModeNormal, DirectoryFilters: []string{"-node_modules"}, TemplateExtensions: []string{}, }, UIOptions: UIOptions{ DiagnosticOptions: DiagnosticOptions{ DiagnosticsDelay: 250 * time.Millisecond, Annotations: map[Annotation]bool{ Bounds: true, Escape: true, Inline: true, Nil: true, }, }, InlayHintOptions: InlayHintOptions{}, DocumentationOptions: DocumentationOptions{ HoverKind: FullDocumentation, LinkTarget: "pkg.go.dev", LinksInHover: true, }, NavigationOptions: NavigationOptions{ ImportShortcut: Both, SymbolMatcher: SymbolFastFuzzy, SymbolStyle: DynamicSymbols, }, CompletionOptions: CompletionOptions{ Matcher: Fuzzy, CompletionBudget: 100 * time.Millisecond, ExperimentalPostfixCompletions: true, }, Codelenses: map[string]bool{ string(command.Generate): true, string(command.RegenerateCgo): true, string(command.Tidy): true, string(command.GCDetails): false, string(command.UpgradeDependency): true, string(command.Vendor): true, }, }, }, InternalOptions: InternalOptions{ LiteralCompletions: true, TempModfile: true, CompleteUnimported: true, CompletionDocumentation: true, DeepCompletion: true, }, Hooks: Hooks{ ComputeEdits: myers.ComputeEdits, URLRegexp: urlRegexp(), DefaultAnalyzers: defaultAnalyzers(), TypeErrorAnalyzers: typeErrorAnalyzers(), ConvenienceAnalyzers: convenienceAnalyzers(), StaticcheckAnalyzers: map[string]*Analyzer{}, GoDiff: true, }, } }) return defaultOptions } // Options holds various configuration that affects Gopls execution, organized // by the nature or origin of the settings. type Options struct { ClientOptions ServerOptions UserOptions InternalOptions Hooks } // ClientOptions holds LSP-specific configuration that is provided by the // client. type ClientOptions struct { InsertTextFormat protocol.InsertTextFormat ConfigurationSupported bool DynamicConfigurationSupported bool DynamicRegistrationSemanticTokensSupported bool DynamicWatchedFilesSupported bool PreferredContentFormat protocol.MarkupKind LineFoldingOnly bool HierarchicalDocumentSymbolSupport bool SemanticTypes []string SemanticMods []string RelatedInformationSupported bool CompletionTags bool CompletionDeprecated bool } // ServerOptions holds LSP-specific configuration that is provided by the // server. type ServerOptions struct { SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool SupportedCommands []string } type BuildOptions struct { // BuildFlags is the set of flags passed on to the build system when invoked. // It is applied to queries like `go list`, which is used when discovering files. // The most common use is to set `-tags`. BuildFlags []string // Env adds environment variables to external commands run by `gopls`, most notably `go list`. Env map[string]string // DirectoryFilters can be used to exclude unwanted directories from the // workspace. By default, all directories are included. Filters are an // operator, `+` to include and `-` to exclude, followed by a path prefix // relative to the workspace folder. They are evaluated in order, and // the last filter that applies to a path controls whether it is included. // The path prefix can be empty, so an initial `-` excludes everything. // // Examples: // // Exclude node_modules: `-node_modules` // // Include only project_a: `-` (exclude everything), `+project_a` // // Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules` DirectoryFilters []string // TemplateExtensions gives the extensions of file names that are treateed // as template files. (The extension // is the part of the file name after the final dot.) TemplateExtensions []string // MemoryMode controls the tradeoff `gopls` makes between memory usage and // correctness. // // Values other than `Normal` are untested and may break in surprising ways. MemoryMode MemoryMode `status:"experimental"` // ExpandWorkspaceToModule instructs `gopls` to adjust the scope of the // workspace to find the best available module root. `gopls` first looks for // a go.mod file in any parent directory of the workspace folder, expanding // the scope to that directory if it exists. If no viable parent directory is // found, gopls will check if there is exactly one child directory containing // a go.mod file, narrowing the scope to that directory if it exists. ExpandWorkspaceToModule bool `status:"experimental"` // ExperimentalWorkspaceModule opts a user into the experimental support // for multi-module workspaces. ExperimentalWorkspaceModule bool `status:"experimental"` // ExperimentalPackageCacheKey controls whether to use a coarser cache key // for package type information to increase cache hits. This setting removes // the user's environment, build flags, and working directory from the cache // key, which should be a safe change as all relevant inputs into the type // checking pass are already hashed into the key. This is temporarily guarded // by an experiment because caching behavior is subtle and difficult to // comprehensively test. ExperimentalPackageCacheKey bool `status:"experimental"` // AllowModfileModifications disables -mod=readonly, allowing imports from // out-of-scope modules. This option will eventually be removed. AllowModfileModifications bool `status:"experimental"` // AllowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module // downloads rather than requiring user action. This option will eventually // be removed. AllowImplicitNetworkAccess bool `status:"experimental"` // ExperimentalUseInvalidMetadata enables gopls to fall back on outdated // package metadata to provide editor features if the go command fails to // load packages for some reason (like an invalid go.mod file). This will // eventually be the default behavior, and this setting will be removed. ExperimentalUseInvalidMetadata bool `status:"experimental"` } type UIOptions struct { DocumentationOptions CompletionOptions NavigationOptions DiagnosticOptions InlayHintOptions // Codelenses overrides the enabled/disabled state of code lenses. See the // "Code Lenses" section of the // [Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses) // for the list of supported lenses. // // Example Usage: // // ```json5 // "gopls": { // ... // "codelenses": { // "generate": false, // Don't show the `go generate` lens. // "gc_details": true // Show a code lens toggling the display of gc's choices. // } // ... // } // ``` Codelenses map[string]bool // SemanticTokens controls whether the LSP server will send // semantic tokens to the client. SemanticTokens bool `status:"experimental"` } type CompletionOptions struct { // Placeholders enables placeholders for function parameters or struct // fields in completion responses. UsePlaceholders bool // CompletionBudget is the soft latency goal for completion requests. Most // requests finish in a couple milliseconds, but in some cases deep // completions can take much longer. As we use up our budget we // dynamically reduce the search scope to ensure we return timely // results. Zero means unlimited. CompletionBudget time.Duration `status:"debug"` // Matcher sets the algorithm that is used when calculating completion // candidates. Matcher Matcher `status:"advanced"` // ExperimentalPostfixCompletions enables artificial method snippets // such as "someSlice.sort!". ExperimentalPostfixCompletions bool `status:"experimental"` } type DocumentationOptions struct { // HoverKind controls the information that appears in the hover text. // SingleLine and Structured are intended for use only by authors of editor plugins. HoverKind HoverKind // LinkTarget controls where documentation links go. // It might be one of: // // * `"godoc.org"` // * `"pkg.go.dev"` // // If company chooses to use its own `godoc.org`, its address can be used as well. LinkTarget string // LinksInHover toggles the presence of links to documentation in hover. LinksInHover bool } type FormattingOptions struct { // Local is the equivalent of the `goimports -local` flag, which puts // imports beginning with this string after third-party packages. It should // be the prefix of the import path whose imports should be grouped // separately. Local string // Gofumpt indicates if we should run gofumpt formatting. Gofumpt bool } type DiagnosticOptions struct { // Analyses specify analyses that the user would like to enable or disable. // A map of the names of analysis passes that should be enabled/disabled. // A full list of analyzers that gopls uses can be found // [here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). // // Example Usage: // // ```json5 // ... // "analyses": { // "unreachable": false, // Disable the unreachable analyzer. // "unusedparams": true // Enable the unusedparams analyzer. // } // ... // ``` Analyses map[string]bool // Staticcheck enables additional analyses from staticcheck.io. Staticcheck bool `status:"experimental"` // Annotations specifies the various kinds of optimization diagnostics // that should be reported by the gc_details command. Annotations map[Annotation]bool `status:"experimental"` // DiagnosticsDelay controls the amount of time that gopls waits // after the most recent file modification before computing deep diagnostics. // Simple diagnostics (parsing and type-checking) are always run immediately // on recently modified packages. // // This option must be set to a valid duration string, for example `"250ms"`. DiagnosticsDelay time.Duration `status:"advanced"` // ExperimentalWatchedFileDelay controls the amount of time that gopls waits // for additional workspace/didChangeWatchedFiles notifications to arrive, // before processing all such notifications in a single batch. This is // intended for use by LSP clients that don't support their own batching of // file system notifications. // // This option must be set to a valid duration string, for example `"100ms"`. ExperimentalWatchedFileDelay time.Duration `status:"experimental"` } type InlayHintOptions struct { // Hints specify inlay hints that users want to see. // A full list of hints that gopls uses can be found // [here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). Hints map[string]bool `status:"experimental"` } type NavigationOptions struct { // ImportShortcut specifies whether import statements should link to // documentation or go to definitions. ImportShortcut ImportShortcut // SymbolMatcher sets the algorithm that is used when finding workspace symbols. SymbolMatcher SymbolMatcher `status:"advanced"` // SymbolStyle controls how symbols are qualified in symbol responses. // // Example Usage: // // ```json5 // "gopls": { // ... // "symbolStyle": "Dynamic", // ... // } // ``` SymbolStyle SymbolStyle `status:"advanced"` } // UserOptions holds custom Gopls configuration (not part of the LSP) that is // modified by the client. type UserOptions struct { BuildOptions UIOptions FormattingOptions // VerboseOutput enables additional debug logging. VerboseOutput bool `status:"debug"` } // EnvSlice returns Env as a slice of k=v strings. func (u *UserOptions) EnvSlice() []string { var result []string for k, v := range u.Env { result = append(result, fmt.Sprintf("%v=%v", k, v)) } return result } // SetEnvSlice sets Env from a slice of k=v strings. func (u *UserOptions) SetEnvSlice(env []string) { u.Env = map[string]string{} for _, kv := range env { split := strings.SplitN(kv, "=", 2) if len(split) != 2 { continue } u.Env[split[0]] = split[1] } } // Hooks contains configuration that is provided to the Gopls command by the // main package. type Hooks struct { // LicensesText holds third party licenses for software used by gopls. LicensesText string // TODO(rfindley): is this even necessary? GoDiff bool // Whether staticcheck is supported. StaticcheckSupported bool // ComputeEdits is used to compute edits between file versions. ComputeEdits diff.ComputeEdits // URLRegexp is used to find potential URLs in comments/strings. // // Not all matches are shown to the user: if the matched URL is not detected // as valid, it will be skipped. URLRegexp *regexp.Regexp // GofumptFormat allows the gopls module to wire-in a call to // gofumpt/format.Source. langVersion and modulePath are used for some // Gofumpt formatting rules -- see the Gofumpt documentation for details. GofumptFormat func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) DefaultAnalyzers map[string]*Analyzer TypeErrorAnalyzers map[string]*Analyzer ConvenienceAnalyzers map[string]*Analyzer StaticcheckAnalyzers map[string]*Analyzer // Govulncheck is the implementation of the Govulncheck gopls command. Govulncheck func(context.Context, *packages.Config, command.VulncheckArgs) (command.VulncheckResult, error) } // InternalOptions contains settings that are not intended for use by the // average user. These may be settings used by tests or outdated settings that // will soon be deprecated. Some of these settings may not even be configurable // by the user. type InternalOptions struct { // LiteralCompletions controls whether literal candidates such as // "&someStruct{}" are offered. Tests disable this flag to simplify // their expected values. LiteralCompletions bool // VerboseWorkDoneProgress controls whether the LSP server should send // progress reports for all work done outside the scope of an RPC. // Used by the regression tests. VerboseWorkDoneProgress bool // The following options were previously available to users, but they // really shouldn't be configured by anyone other than "power users". // CompletionDocumentation enables documentation with completion results. CompletionDocumentation bool // CompleteUnimported enables completion for packages that you do not // currently import. CompleteUnimported bool // DeepCompletion enables the ability to return completions from deep // inside relevant entities, rather than just the locally accessible ones. // // Consider this example: // // ```go // package main // // import "fmt" // // type wrapString struct { // str string // } // // func main() { // x := wrapString{"hello world"} // fmt.Printf(<>) // } // ``` // // At the location of the `<>` in this program, deep completion would suggest // the result `x.str`. DeepCompletion bool // TempModfile controls the use of the -modfile flag in Go 1.14. TempModfile bool // ShowBugReports causes a message to be shown when the first bug is reported // on the server. // This option applies only during initialization. ShowBugReports bool } type ImportShortcut string const ( Both ImportShortcut = "Both" Link ImportShortcut = "Link" Definition ImportShortcut = "Definition" ) func (s ImportShortcut) ShowLinks() bool { return s == Both || s == Link } func (s ImportShortcut) ShowDefinition() bool { return s == Both || s == Definition } type Matcher string const ( Fuzzy Matcher = "Fuzzy" CaseInsensitive Matcher = "CaseInsensitive" CaseSensitive Matcher = "CaseSensitive" ) type SymbolMatcher string const ( SymbolFuzzy SymbolMatcher = "Fuzzy" SymbolFastFuzzy SymbolMatcher = "FastFuzzy" SymbolCaseInsensitive SymbolMatcher = "CaseInsensitive" SymbolCaseSensitive SymbolMatcher = "CaseSensitive" ) type SymbolStyle string const ( // PackageQualifiedSymbols is package qualified symbols i.e. // "pkg.Foo.Field". PackageQualifiedSymbols SymbolStyle = "Package" // FullyQualifiedSymbols is fully qualified symbols, i.e. // "path/to/pkg.Foo.Field". FullyQualifiedSymbols SymbolStyle = "Full" // DynamicSymbols uses whichever qualifier results in the highest scoring // match for the given symbol query. Here a "qualifier" is any "/" or "." // delimited suffix of the fully qualified symbol. i.e. "to/pkg.Foo.Field" or // just "Foo.Field". DynamicSymbols SymbolStyle = "Dynamic" ) type HoverKind string const ( SingleLine HoverKind = "SingleLine" NoDocumentation HoverKind = "NoDocumentation" SynopsisDocumentation HoverKind = "SynopsisDocumentation" FullDocumentation HoverKind = "FullDocumentation" // Structured is an experimental setting that returns a structured hover format. // This format separates the signature from the documentation, so that the client // can do more manipulation of these fields. // // This should only be used by clients that support this behavior. Structured HoverKind = "Structured" ) type MemoryMode string const ( ModeNormal MemoryMode = "Normal" // In DegradeClosed mode, `gopls` will collect less information about // packages without open files. As a result, features like Find // References and Rename will miss results in such packages. ModeDegradeClosed MemoryMode = "DegradeClosed" ) type OptionResults []OptionResult type OptionResult struct { Name string Value interface{} Error error } type OptionState int const ( OptionHandled = OptionState(iota) OptionDeprecated OptionUnexpected ) type LinkTarget string func SetOptions(options *Options, opts interface{}) OptionResults { var results OptionResults switch opts := opts.(type) { case nil: case map[string]interface{}: // If the user's settings contains "allExperiments", set that first, // and then let them override individual settings independently. var enableExperiments bool for name, value := range opts { if b, ok := value.(bool); name == "allExperiments" && ok && b { enableExperiments = true options.EnableAllExperiments() } } seen := map[string]struct{}{} for name, value := range opts { results = append(results, options.set(name, value, seen)) } // Finally, enable any experimental features that are specified in // maps, which allows users to individually toggle them on or off. if enableExperiments { options.enableAllExperimentMaps() } default: results = append(results, OptionResult{ Value: opts, Error: fmt.Errorf("Invalid options type %T", opts), }) } return results } func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) { // Check if the client supports snippets in completion items. if c := caps.TextDocument.Completion; c.CompletionItem.SnippetSupport { o.InsertTextFormat = protocol.SnippetTextFormat } // Check if the client supports configuration messages. o.ConfigurationSupported = caps.Workspace.Configuration o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration o.DynamicRegistrationSemanticTokensSupported = caps.TextDocument.SemanticTokens.DynamicRegistration o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration // Check which types of content format are supported by this client. if hover := caps.TextDocument.Hover; len(hover.ContentFormat) > 0 { o.PreferredContentFormat = hover.ContentFormat[0] } // Check if the client supports only line folding. fr := caps.TextDocument.FoldingRange o.LineFoldingOnly = fr.LineFoldingOnly // Check if the client supports hierarchical document symbols. o.HierarchicalDocumentSymbolSupport = caps.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport // Check if the client supports semantic tokens o.SemanticTypes = caps.TextDocument.SemanticTokens.TokenTypes o.SemanticMods = caps.TextDocument.SemanticTokens.TokenModifiers // we don't need Requests, as we support full functionality // we don't need Formats, as there is only one, for now // Check if the client supports diagnostic related information. o.RelatedInformationSupported = caps.TextDocument.PublishDiagnostics.RelatedInformation // Check if the client completion support includes tags (preferred) or deprecation if caps.TextDocument.Completion.CompletionItem.TagSupport.ValueSet != nil { o.CompletionTags = true } else if caps.TextDocument.Completion.CompletionItem.DeprecatedSupport { o.CompletionDeprecated = true } } func (o *Options) Clone() *Options { result := &Options{ ClientOptions: o.ClientOptions, InternalOptions: o.InternalOptions, Hooks: Hooks{ GoDiff: o.GoDiff, StaticcheckSupported: o.StaticcheckSupported, ComputeEdits: o.ComputeEdits, GofumptFormat: o.GofumptFormat, URLRegexp: o.URLRegexp, Govulncheck: o.Govulncheck, }, ServerOptions: o.ServerOptions, UserOptions: o.UserOptions, } // Fully clone any slice or map fields. Only Hooks, ExperimentalOptions, // and UserOptions can be modified. copyStringMap := func(src map[string]bool) map[string]bool { dst := make(map[string]bool) for k, v := range src { dst[k] = v } return dst } result.Analyses = copyStringMap(o.Analyses) result.Codelenses = copyStringMap(o.Codelenses) copySlice := func(src []string) []string { dst := make([]string, len(src)) copy(dst, src) return dst } result.SetEnvSlice(o.EnvSlice()) result.BuildFlags = copySlice(o.BuildFlags) result.DirectoryFilters = copySlice(o.DirectoryFilters) copyAnalyzerMap := func(src map[string]*Analyzer) map[string]*Analyzer { dst := make(map[string]*Analyzer) for k, v := range src { dst[k] = v } return dst } result.DefaultAnalyzers = copyAnalyzerMap(o.DefaultAnalyzers) result.TypeErrorAnalyzers = copyAnalyzerMap(o.TypeErrorAnalyzers) result.ConvenienceAnalyzers = copyAnalyzerMap(o.ConvenienceAnalyzers) result.StaticcheckAnalyzers = copyAnalyzerMap(o.StaticcheckAnalyzers) return result } func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer, enabled bool, severity protocol.DiagnosticSeverity) { o.StaticcheckAnalyzers[a.Name] = &Analyzer{ Analyzer: a, Enabled: enabled, Severity: severity, } } // EnableAllExperiments turns on all of the experimental "off-by-default" // features offered by gopls. Any experimental features specified in maps // should be enabled in enableAllExperimentMaps. func (o *Options) EnableAllExperiments() { o.SemanticTokens = true o.ExperimentalPostfixCompletions = true o.ExperimentalUseInvalidMetadata = true o.ExperimentalWatchedFileDelay = 50 * time.Millisecond o.SymbolMatcher = SymbolFastFuzzy } func (o *Options) enableAllExperimentMaps() { if _, ok := o.Codelenses[string(command.GCDetails)]; !ok { o.Codelenses[string(command.GCDetails)] = true } if _, ok := o.Analyses[unusedparams.Analyzer.Name]; !ok { o.Analyses[unusedparams.Analyzer.Name] = true } } func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult { // Flatten the name in case we get options with a hierarchy. split := strings.Split(name, ".") name = split[len(split)-1] result := OptionResult{Name: name, Value: value} if _, ok := seen[name]; ok { result.errorf("duplicate configuration for %s", name) } seen[name] = struct{}{} switch name { case "env": menv, ok := value.(map[string]interface{}) if !ok { result.errorf("invalid type %T, expect map", value) break } if o.Env == nil { o.Env = make(map[string]string) } for k, v := range menv { o.Env[k] = fmt.Sprint(v) } case "buildFlags": iflags, ok := value.([]interface{}) if !ok { result.errorf("invalid type %T, expect list", value) break } flags := make([]string, 0, len(iflags)) for _, flag := range iflags { flags = append(flags, fmt.Sprintf("%s", flag)) } o.BuildFlags = flags case "directoryFilters": ifilters, ok := value.([]interface{}) if !ok { result.errorf("invalid type %T, expect list", value) break } var filters []string for _, ifilter := range ifilters { filter := fmt.Sprint(ifilter) if filter == "" || (filter[0] != '+' && filter[0] != '-') { result.errorf("invalid filter %q, must start with + or -", filter) return result } filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) } o.DirectoryFilters = filters case "memoryMode": if s, ok := result.asOneOf( string(ModeNormal), string(ModeDegradeClosed), ); ok { o.MemoryMode = MemoryMode(s) } case "completionDocumentation": result.setBool(&o.CompletionDocumentation) case "usePlaceholders": result.setBool(&o.UsePlaceholders) case "deepCompletion": result.setBool(&o.DeepCompletion) case "completeUnimported": result.setBool(&o.CompleteUnimported) case "completionBudget": result.setDuration(&o.CompletionBudget) case "matcher": if s, ok := result.asOneOf( string(Fuzzy), string(CaseSensitive), string(CaseInsensitive), ); ok { o.Matcher = Matcher(s) } case "symbolMatcher": if s, ok := result.asOneOf( string(SymbolFuzzy), string(SymbolFastFuzzy), string(SymbolCaseInsensitive), string(SymbolCaseSensitive), ); ok { o.SymbolMatcher = SymbolMatcher(s) } case "symbolStyle": if s, ok := result.asOneOf( string(FullyQualifiedSymbols), string(PackageQualifiedSymbols), string(DynamicSymbols), ); ok { o.SymbolStyle = SymbolStyle(s) } case "hoverKind": if s, ok := result.asOneOf( string(NoDocumentation), string(SingleLine), string(SynopsisDocumentation), string(FullDocumentation), string(Structured), ); ok { o.HoverKind = HoverKind(s) } case "linkTarget": result.setString(&o.LinkTarget) case "linksInHover": result.setBool(&o.LinksInHover) case "importShortcut": if s, ok := result.asOneOf(string(Both), string(Link), string(Definition)); ok { o.ImportShortcut = ImportShortcut(s) } case "analyses": result.setBoolMap(&o.Analyses) case "hints": result.setBoolMap(&o.Hints) case "annotations": result.setAnnotationMap(&o.Annotations) case "codelenses", "codelens": var lensOverrides map[string]bool result.setBoolMap(&lensOverrides) if result.Error == nil { if o.Codelenses == nil { o.Codelenses = make(map[string]bool) } for lens, enabled := range lensOverrides { o.Codelenses[lens] = enabled } } // codelens is deprecated, but still works for now. // TODO(rstambler): Remove this for the gopls/v0.7.0 release. if name == "codelens" { result.deprecated("codelenses") } case "staticcheck": if v, ok := result.asBool(); ok { o.Staticcheck = v if v && !o.StaticcheckSupported { // Warn if the user is trying to enable staticcheck, but staticcheck is // unsupported. result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s\n"+ "\trebuild gopls with a more recent version of Go", result.Name, runtime.Version()) } } case "local": result.setString(&o.Local) case "verboseOutput": result.setBool(&o.VerboseOutput) case "verboseWorkDoneProgress": result.setBool(&o.VerboseWorkDoneProgress) case "tempModfile": result.setBool(&o.TempModfile) case "showBugReports": result.setBool(&o.ShowBugReports) case "gofumpt": result.setBool(&o.Gofumpt) case "semanticTokens": result.setBool(&o.SemanticTokens) case "expandWorkspaceToModule": result.setBool(&o.ExpandWorkspaceToModule) case "experimentalPostfixCompletions": result.setBool(&o.ExperimentalPostfixCompletions) case "experimentalWorkspaceModule": // TODO(rfindley): suggest go.work on go1.18+ result.setBool(&o.ExperimentalWorkspaceModule) case "experimentalTemplateSupport": // TODO(pjw): remove after June 2022 result.deprecated("") case "templateExtensions": if iexts, ok := value.([]interface{}); ok { ans := []string{} for _, x := range iexts { ans = append(ans, fmt.Sprint(x)) } o.TemplateExtensions = ans break } if value == nil { o.TemplateExtensions = nil break } result.errorf(fmt.Sprintf("unexpected type %T not []string", value)) case "experimentalDiagnosticsDelay", "diagnosticsDelay": if name == "experimentalDiagnosticsDelay" { result.deprecated("diagnosticsDelay") } result.setDuration(&o.DiagnosticsDelay) case "experimentalWatchedFileDelay": result.setDuration(&o.ExperimentalWatchedFileDelay) case "experimentalPackageCacheKey": result.setBool(&o.ExperimentalPackageCacheKey) case "allowModfileModifications": result.setBool(&o.AllowModfileModifications) case "allowImplicitNetworkAccess": result.setBool(&o.AllowImplicitNetworkAccess) case "experimentalUseInvalidMetadata": result.setBool(&o.ExperimentalUseInvalidMetadata) case "allExperiments": // This setting should be handled before all of the other options are // processed, so do nothing here. // Replaced settings. case "experimentalDisabledAnalyses": result.deprecated("analyses") case "disableDeepCompletion": result.deprecated("deepCompletion") case "disableFuzzyMatching": result.deprecated("fuzzyMatching") case "wantCompletionDocumentation": result.deprecated("completionDocumentation") case "wantUnimportedCompletions": result.deprecated("completeUnimported") case "fuzzyMatching": result.deprecated("matcher") case "caseSensitiveCompletion": result.deprecated("matcher") // Deprecated settings. case "wantSuggestedFixes": result.deprecated("") case "noIncrementalSync": result.deprecated("") case "watchFileChanges": result.deprecated("") case "go-diff": result.deprecated("") default: result.unexpected() } return result } func (r *OptionResult) errorf(msg string, values ...interface{}) { prefix := fmt.Sprintf("parsing setting %q: ", r.Name) r.Error = fmt.Errorf(prefix+msg, values...) } // A SoftError is an error that does not affect the functionality of gopls. type SoftError struct { msg string } func (e *SoftError) Error() string { return e.msg } func (r *OptionResult) deprecated(replacement string) { msg := fmt.Sprintf("gopls setting %q is deprecated", r.Name) if replacement != "" { msg = fmt.Sprintf("%s, use %q instead", msg, replacement) } r.Error = &SoftError{msg} } func (r *OptionResult) unexpected() { r.Error = fmt.Errorf("unexpected gopls setting %q", r.Name) } func (r *OptionResult) asBool() (bool, bool) { b, ok := r.Value.(bool) if !ok { r.errorf("invalid type %T, expect bool", r.Value) return false, false } return b, true } func (r *OptionResult) setBool(b *bool) { if v, ok := r.asBool(); ok { *b = v } } func (r *OptionResult) setDuration(d *time.Duration) { if v, ok := r.asString(); ok { parsed, err := time.ParseDuration(v) if err != nil { r.errorf("failed to parse duration %q: %v", v, err) return } *d = parsed } } func (r *OptionResult) setBoolMap(bm *map[string]bool) { m := r.asBoolMap() *bm = m } func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) { all := r.asBoolMap() if all == nil { return } // Default to everything enabled by default. m := make(map[Annotation]bool) for k, enabled := range all { a, err := asOneOf( k, string(Nil), string(Escape), string(Inline), string(Bounds), ) if err != nil { // In case of an error, process any legacy values. switch k { case "noEscape": m[Escape] = false r.errorf(`"noEscape" is deprecated, set "Escape: false" instead`) case "noNilcheck": m[Nil] = false r.errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) case "noInline": m[Inline] = false r.errorf(`"noInline" is deprecated, set "Inline: false" instead`) case "noBounds": m[Bounds] = false r.errorf(`"noBounds" is deprecated, set "Bounds: false" instead`) default: r.errorf(err.Error()) } continue } m[Annotation(a)] = enabled } *bm = m } func (r *OptionResult) asBoolMap() map[string]bool { all, ok := r.Value.(map[string]interface{}) if !ok { r.errorf("invalid type %T for map[string]bool option", r.Value) return nil } m := make(map[string]bool) for a, enabled := range all { if enabled, ok := enabled.(bool); ok { m[a] = enabled } else { r.errorf("invalid type %T for map key %q", enabled, a) return m } } return m } func (r *OptionResult) asString() (string, bool) { b, ok := r.Value.(string) if !ok { r.errorf("invalid type %T, expect string", r.Value) return "", false } return b, true } func (r *OptionResult) asOneOf(options ...string) (string, bool) { s, ok := r.asString() if !ok { return "", false } s, err := asOneOf(s, options...) if err != nil { r.errorf(err.Error()) } return s, err == nil } func asOneOf(str string, options ...string) (string, error) { lower := strings.ToLower(str) for _, opt := range options { if strings.ToLower(opt) == lower { return opt, nil } } return "", fmt.Errorf("invalid option %q for enum", str) } func (r *OptionResult) setString(s *string) { if v, ok := r.asString(); ok { *s = v } } // EnabledAnalyzers returns all of the analyzers enabled for the given // snapshot. func EnabledAnalyzers(snapshot Snapshot) (analyzers []*Analyzer) { for _, a := range snapshot.View().Options().DefaultAnalyzers { if a.IsEnabled(snapshot.View()) { analyzers = append(analyzers, a) } } for _, a := range snapshot.View().Options().TypeErrorAnalyzers { if a.IsEnabled(snapshot.View()) { analyzers = append(analyzers, a) } } for _, a := range snapshot.View().Options().ConvenienceAnalyzers { if a.IsEnabled(snapshot.View()) { analyzers = append(analyzers, a) } } for _, a := range snapshot.View().Options().StaticcheckAnalyzers { if a.IsEnabled(snapshot.View()) { analyzers = append(analyzers, a) } } return analyzers } func typeErrorAnalyzers() map[string]*Analyzer { return map[string]*Analyzer{ fillreturns.Analyzer.Name: { Analyzer: fillreturns.Analyzer, ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, Enabled: true, }, nonewvars.Analyzer.Name: { Analyzer: nonewvars.Analyzer, Enabled: true, }, noresultvalues.Analyzer.Name: { Analyzer: noresultvalues.Analyzer, Enabled: true, }, undeclaredname.Analyzer.Name: { Analyzer: undeclaredname.Analyzer, Fix: UndeclaredName, Enabled: true, }, } } func convenienceAnalyzers() map[string]*Analyzer { return map[string]*Analyzer{ fillstruct.Analyzer.Name: { Analyzer: fillstruct.Analyzer, Fix: FillStruct, Enabled: true, ActionKind: []protocol.CodeActionKind{protocol.RefactorRewrite}, }, stubmethods.Analyzer.Name: { Analyzer: stubmethods.Analyzer, ActionKind: []protocol.CodeActionKind{protocol.RefactorRewrite}, Fix: StubMethods, Enabled: true, }, } } func defaultAnalyzers() map[string]*Analyzer { return map[string]*Analyzer{ // The traditional vet suite: asmdecl.Analyzer.Name: {Analyzer: asmdecl.Analyzer, Enabled: true}, assign.Analyzer.Name: {Analyzer: assign.Analyzer, Enabled: true}, atomic.Analyzer.Name: {Analyzer: atomic.Analyzer, Enabled: true}, bools.Analyzer.Name: {Analyzer: bools.Analyzer, Enabled: true}, buildtag.Analyzer.Name: {Analyzer: buildtag.Analyzer, Enabled: true}, cgocall.Analyzer.Name: {Analyzer: cgocall.Analyzer, Enabled: true}, composite.Analyzer.Name: {Analyzer: composite.Analyzer, Enabled: true}, copylock.Analyzer.Name: {Analyzer: copylock.Analyzer, Enabled: true}, errorsas.Analyzer.Name: {Analyzer: errorsas.Analyzer, Enabled: true}, httpresponse.Analyzer.Name: {Analyzer: httpresponse.Analyzer, Enabled: true}, ifaceassert.Analyzer.Name: {Analyzer: ifaceassert.Analyzer, Enabled: true}, loopclosure.Analyzer.Name: {Analyzer: loopclosure.Analyzer, Enabled: true}, lostcancel.Analyzer.Name: {Analyzer: lostcancel.Analyzer, Enabled: true}, nilfunc.Analyzer.Name: {Analyzer: nilfunc.Analyzer, Enabled: true}, printf.Analyzer.Name: {Analyzer: printf.Analyzer, Enabled: true}, shift.Analyzer.Name: {Analyzer: shift.Analyzer, Enabled: true}, stdmethods.Analyzer.Name: {Analyzer: stdmethods.Analyzer, Enabled: true}, stringintconv.Analyzer.Name: {Analyzer: stringintconv.Analyzer, Enabled: true}, structtag.Analyzer.Name: {Analyzer: structtag.Analyzer, Enabled: true}, tests.Analyzer.Name: {Analyzer: tests.Analyzer, Enabled: true}, unmarshal.Analyzer.Name: {Analyzer: unmarshal.Analyzer, Enabled: true}, unreachable.Analyzer.Name: {Analyzer: unreachable.Analyzer, Enabled: true}, unsafeptr.Analyzer.Name: {Analyzer: unsafeptr.Analyzer, Enabled: true}, unusedresult.Analyzer.Name: {Analyzer: unusedresult.Analyzer, Enabled: true}, // Non-vet analyzers: atomicalign.Analyzer.Name: {Analyzer: atomicalign.Analyzer, Enabled: true}, deepequalerrors.Analyzer.Name: {Analyzer: deepequalerrors.Analyzer, Enabled: true}, fieldalignment.Analyzer.Name: {Analyzer: fieldalignment.Analyzer, Enabled: false}, nilness.Analyzer.Name: {Analyzer: nilness.Analyzer, Enabled: false}, shadow.Analyzer.Name: {Analyzer: shadow.Analyzer, Enabled: false}, sortslice.Analyzer.Name: {Analyzer: sortslice.Analyzer, Enabled: true}, testinggoroutine.Analyzer.Name: {Analyzer: testinggoroutine.Analyzer, Enabled: true}, unusedparams.Analyzer.Name: {Analyzer: unusedparams.Analyzer, Enabled: false}, unusedwrite.Analyzer.Name: {Analyzer: unusedwrite.Analyzer, Enabled: false}, useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false}, infertypeargs.Analyzer.Name: {Analyzer: infertypeargs.Analyzer, Enabled: true}, embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true}, // gofmt -s suite: simplifycompositelit.Analyzer.Name: { Analyzer: simplifycompositelit.Analyzer, Enabled: true, ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, }, simplifyrange.Analyzer.Name: { Analyzer: simplifyrange.Analyzer, Enabled: true, ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, }, simplifyslice.Analyzer.Name: { Analyzer: simplifyslice.Analyzer, Enabled: true, ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, }, } } func urlRegexp() *regexp.Regexp { // Ensure links are matched as full words, not anywhere. re := regexp.MustCompile(`\b(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?\b`) re.Longest() return re } type APIJSON struct { Options map[string][]*OptionJSON Commands []*CommandJSON Lenses []*LensJSON Analyzers []*AnalyzerJSON Hints []*HintJSON } type OptionJSON struct { Name string Type string Doc string EnumKeys EnumKeys EnumValues []EnumValue Default string Status string Hierarchy string } func (o *OptionJSON) String() string { return o.Name } func (o *OptionJSON) Write(w io.Writer) { fmt.Fprintf(w, "**%v** *%v*\n\n", o.Name, o.Type) writeStatus(w, o.Status) enumValues := collectEnums(o) fmt.Fprintf(w, "%v%v\nDefault: `%v`.\n\n", o.Doc, enumValues, o.Default) } func writeStatus(section io.Writer, status string) { switch status { case "": case "advanced": fmt.Fprint(section, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n") case "debug": fmt.Fprint(section, "**This setting is for debugging purposes only.**\n\n") case "experimental": fmt.Fprint(section, "**This setting is experimental and may be deleted.**\n\n") default: fmt.Fprintf(section, "**Status: %s.**\n\n", status) } } var parBreakRE = regexp.MustCompile("\n{2,}") func collectEnums(opt *OptionJSON) string { var b strings.Builder write := func(name, doc string, index, len int) { if doc != "" { unbroken := parBreakRE.ReplaceAllString(doc, "\\\n") fmt.Fprintf(&b, "* %s\n", strings.TrimSpace(unbroken)) } else { fmt.Fprintf(&b, "* `%s`\n", name) } } if len(opt.EnumValues) > 0 && opt.Type == "enum" { b.WriteString("\nMust be one of:\n\n") for i, val := range opt.EnumValues { write(val.Value, val.Doc, i, len(opt.EnumValues)) } } else if len(opt.EnumKeys.Keys) > 0 && shouldShowEnumKeysInSettings(opt.Name) { b.WriteString("\nCan contain any of:\n\n") for i, val := range opt.EnumKeys.Keys { write(val.Name, val.Doc, i, len(opt.EnumKeys.Keys)) } } return b.String() } func shouldShowEnumKeysInSettings(name string) bool { // These fields have too many possible options to print. return !(name == "analyses" || name == "codelenses" || name == "hints") } type EnumKeys struct { ValueType string Keys []EnumKey } type EnumKey struct { Name string Doc string Default string } type EnumValue struct { Value string Doc string } type CommandJSON struct { Command string Title string Doc string ArgDoc string ResultDoc string } func (c *CommandJSON) String() string { return c.Command } func (c *CommandJSON) Write(w io.Writer) { fmt.Fprintf(w, "### **%v**\nIdentifier: `%v`\n\n%v\n\n", c.Title, c.Command, c.Doc) if c.ArgDoc != "" { fmt.Fprintf(w, "Args:\n\n```\n%s\n```\n\n", c.ArgDoc) } if c.ResultDoc != "" { fmt.Fprintf(w, "Result:\n\n```\n%s\n```\n\n", c.ResultDoc) } } type LensJSON struct { Lens string Title string Doc string } func (l *LensJSON) String() string { return l.Title } func (l *LensJSON) Write(w io.Writer) { fmt.Fprintf(w, "%s (%s): %s", l.Title, l.Lens, l.Doc) } type AnalyzerJSON struct { Name string Doc string Default bool } func (a *AnalyzerJSON) String() string { return a.Name } func (a *AnalyzerJSON) Write(w io.Writer) { fmt.Fprintf(w, "%s (%s): %v", a.Name, a.Doc, a.Default) } type HintJSON struct { Name string Doc string Default bool } func (h *HintJSON) String() string { return h.Name } func (h *HintJSON) Write(w io.Writer) { fmt.Fprintf(w, "%s (%s): %v", h.Name, h.Doc, h.Default) }