// 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/defers"
	"golang.org/x/tools/go/analysis/passes/directive"
	"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/slog"
	"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/timeformat"
	"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/gopls/internal/lsp/analysis/deprecated"
	"golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective"
	"golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns"
	"golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct"
	"golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs"
	"golang.org/x/tools/gopls/internal/lsp/analysis/nonewvars"
	"golang.org/x/tools/gopls/internal/lsp/analysis/noresultvalues"
	"golang.org/x/tools/gopls/internal/lsp/analysis/simplifycompositelit"
	"golang.org/x/tools/gopls/internal/lsp/analysis/simplifyrange"
	"golang.org/x/tools/gopls/internal/lsp/analysis/simplifyslice"
	"golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods"
	"golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname"
	"golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams"
	"golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable"
	"golang.org/x/tools/gopls/internal/lsp/analysis/useany"
	"golang.org/x/tools/gopls/internal/lsp/command"
	"golang.org/x/tools/gopls/internal/lsp/protocol"
	"golang.org/x/tools/internal/diff"
	"golang.org/x/tools/internal/diff/myers"
)

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.RefactorInline:        true,
						protocol.RefactorExtract:       true,
					},
					Mod: {
						protocol.SourceOrganizeImports: true,
						protocol.QuickFix:              true,
					},
					Work: {},
					Sum:  {},
					Tmpl: {},
				},
				SupportedCommands: commands,
			},
			UserOptions: UserOptions{
				BuildOptions: BuildOptions{
					ExpandWorkspaceToModule: true,
					MemoryMode:              ModeNormal,
					DirectoryFilters:        []string{"-**/node_modules"},
					TemplateExtensions:      []string{},
					StandaloneTags:          []string{"ignore"},
				},
				UIOptions: UIOptions{
					DiagnosticOptions: DiagnosticOptions{
						Annotations: map[Annotation]bool{
							Bounds: true,
							Escape: true,
							Inline: true,
							Nil:    true,
						},
						Vulncheck:                 ModeVulncheckOff,
						DiagnosticsDelay:          1 * time.Second,
						AnalysisProgressReporting: true,
					},
					InlayHintOptions: InlayHintOptions{},
					DocumentationOptions: DocumentationOptions{
						HoverKind:    FullDocumentation,
						LinkTarget:   "pkg.go.dev",
						LinksInHover: true,
					},
					NavigationOptions: NavigationOptions{
						ImportShortcut: BothShortcuts,
						SymbolMatcher:  SymbolFastFuzzy,
						SymbolStyle:    DynamicSymbols,
						SymbolScope:    AllSymbolScope,
					},
					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,
						// TODO(hyangah): enable command.RunGovulncheck.
					},
				},
			},
			InternalOptions: InternalOptions{
				LiteralCompletions:          true,
				TempModfile:                 true,
				CompleteUnimported:          true,
				CompletionDocumentation:     true,
				DeepCompletion:              true,
				ChattyDiagnostics:           true,
				NewDiff:                     "new",
				SubdirWatchPatterns:         SubdirWatchPatternsAuto,
				ReportAnalysisProgressAfter: 5 * time.Second,
			},
			Hooks: Hooks{
				// TODO(adonovan): switch to new diff.Strings implementation.
				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
}

// IsAnalyzerEnabled reports whether an analyzer with the given name is
// enabled.
//
// TODO(rfindley): refactor to simplify this function. We no longer need the
// different categories of analyzer.
func (opts *Options) IsAnalyzerEnabled(name string) bool {
	for _, amap := range []map[string]*Analyzer{opts.DefaultAnalyzers, opts.TypeErrorAnalyzers, opts.ConvenienceAnalyzers, opts.StaticcheckAnalyzers} {
		for _, analyzer := range amap {
			if analyzer.Analyzer.Name == name && analyzer.IsEnabled(opts) {
				return true
			}
		}
	}
	return false
}

// ClientOptions holds LSP-specific configuration that is provided by the
// client.
type ClientOptions struct {
	ClientInfo                                 *protocol.Msg_XInitializeParams_clientInfo
	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
	SupportedResourceOperations                []protocol.ResourceOperationKind
}

// 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.
	//
	// DirectoryFilters also supports the `**` operator to match 0 or more directories.
	//
	// Examples:
	//
	// Exclude node_modules at current depth: `-node_modules`
	//
	// Exclude node_modules at any depth: `-**/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"`

	// 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"`

	// 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 {
	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"`

	// NoSemanticString turns off the sending of the semantic token 'string'
	NoSemanticString bool `status:"experimental"`

	// NoSemanticNumber  turns off the sending of the semantic token 'number'
	NoSemanticNumber 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.
	//
	// Modules matching the GOPRIVATE environment variable will not have
	// documentation links in hover.
	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 in
	// [analyzers.md](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.
	// These analyses are documented on
	// [Staticcheck's website](https://staticcheck.io/docs/checks/).
	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"`

	// Vulncheck enables vulnerability scanning.
	Vulncheck VulncheckMode `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"`

	// AnalysisProgressReporting controls whether gopls sends progress
	// notifications when construction of its index of analysis facts is taking a
	// long time. Cancelling these notifications will cancel the indexing task,
	// though it will restart after the next change in the workspace.
	//
	// When a package is opened for the first time and heavyweight analyses such as
	// staticcheck are enabled, it can take a while to construct the index of
	// analysis facts for all its dependencies. The index is cached in the
	// filesystem, so subsequent analysis should be faster.
	AnalysisProgressReporting bool
}

type InlayHintOptions struct {
	// Hints specify inlay hints that users want to see. A full list of hints
	// that gopls uses can be found in
	// [inlayHints.md](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"`

	// SymbolScope controls which packages are searched for workspace/symbol
	// requests. The default value, "workspace", searches only workspace
	// packages. The legacy behavior, "all", causes all loaded packages to be
	// searched, including dependencies; this is more expensive and may return
	// unwanted results.
	SymbolScope SymbolScope
}

// 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]
	}
}

// DiffFunction is the type for a function that produces a set of edits that
// convert from the before content to the after content.
type DiffFunction func(before, after string) []diff.Edit

// 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

	// GoDiff is used in gopls/hooks to get Myers' diff
	GoDiff bool

	// Whether staticcheck is supported.
	StaticcheckSupported bool

	// ComputeEdits is used to compute edits between file versions.
	ComputeEdits DiffFunction

	// 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
}

// 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.
//
// TODO(rfindley): even though these settings are not intended for
// modification, some of them should be surfaced in our documentation.
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

	// NewDiff controls the choice of the new diff implementation. It can be
	// 'new', 'old', or 'both', which is the default. 'both' computes diffs with
	// both algorithms, checks that the new algorithm has worked, and write some
	// summary statistics to a file in os.TmpDir().
	NewDiff string

	// ChattyDiagnostics controls whether to report file diagnostics for each
	// file change. If unset, gopls only reports diagnostics when they change, or
	// when a file is opened or closed.
	ChattyDiagnostics bool

	// SubdirWatchPatterns configures the file watching glob patterns registered
	// by gopls.
	//
	// Some clients (namely VS Code) do not send workspace/didChangeWatchedFile
	// notifications for files contained in a directory when that directory is
	// deleted:
	// https://github.com/microsoft/vscode/issues/109754
	//
	// In this case, gopls would miss important notifications about deleted
	// packages. To work around this, gopls registers a watch pattern for each
	// directory containing Go files.
	//
	// Unfortunately, other clients experience performance problems with this
	// many watch patterns, so there is no single behavior that works well for
	// all clients.
	//
	// The "subdirWatchPatterns" setting allows configuring this behavior. Its
	// default value of "auto" attempts to guess the correct behavior based on
	// the client name. We'd love to avoid this specialization, but as described
	// above there is no single value that works for all clients.
	//
	// If any LSP client does not behave well with the default value (for
	// example, if like VS Code it drops file notifications), please file an
	// issue.
	SubdirWatchPatterns SubdirWatchPatterns

	// ReportAnalysisProgressAfter sets the duration for gopls to wait before starting
	// progress reporting for ongoing go/analysis passes.
	//
	// It is intended to be used for testing only.
	ReportAnalysisProgressAfter time.Duration
}

type SubdirWatchPatterns string

const (
	SubdirWatchPatternsOn   SubdirWatchPatterns = "on"
	SubdirWatchPatternsOff  SubdirWatchPatterns = "off"
	SubdirWatchPatternsAuto SubdirWatchPatterns = "auto"
)

type ImportShortcut string

const (
	BothShortcuts      ImportShortcut = "Both"
	LinkShortcut       ImportShortcut = "Link"
	DefinitionShortcut ImportShortcut = "Definition"
)

func (s ImportShortcut) ShowLinks() bool {
	return s == BothShortcuts || s == LinkShortcut
}

func (s ImportShortcut) ShowDefinition() bool {
	return s == BothShortcuts || s == DefinitionShortcut
}

type Matcher string

const (
	Fuzzy           Matcher = "Fuzzy"
	CaseInsensitive Matcher = "CaseInsensitive"
	CaseSensitive   Matcher = "CaseSensitive"
)

// A SymbolMatcher controls the matching of symbols for workspace/symbol
// requests.
type SymbolMatcher string

const (
	SymbolFuzzy           SymbolMatcher = "Fuzzy"
	SymbolFastFuzzy       SymbolMatcher = "FastFuzzy"
	SymbolCaseInsensitive SymbolMatcher = "CaseInsensitive"
	SymbolCaseSensitive   SymbolMatcher = "CaseSensitive"
)

// A SymbolStyle controls the formatting of symbols in workspace/symbol results.
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"
)

// A SymbolScope controls the search scope for workspace/symbol requests.
type SymbolScope string

const (
	// WorkspaceSymbolScope matches symbols in workspace packages only.
	WorkspaceSymbolScope SymbolScope = "workspace"
	// AllSymbolScope matches symbols in any loaded package, including
	// dependencies.
	AllSymbolScope SymbolScope = "all"
)

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 VulncheckMode string

const (
	// Disable vulnerability analysis.
	ModeVulncheckOff VulncheckMode = "Off"
	// In Imports mode, `gopls` will report vulnerabilities that affect packages
	// directly and indirectly used by the analyzed main module.
	ModeVulncheckImports VulncheckMode = "Imports"

	// TODO: VulncheckRequire, VulncheckCallgraph
)

type OptionResults []OptionResult

type OptionResult struct {
	Name  string
	Value interface{}
	Error error
}

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(clientName *protocol.Msg_XInitializeParams_clientInfo, caps protocol.ClientCapabilities) {
	o.ClientInfo = clientName
	// Check if the client supports snippets in completion items.
	if caps.Workspace.WorkspaceEdit != nil {
		o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations
	}
	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; hover != nil && len(hover.ContentFormat) > 0 {
		o.PreferredContentFormat = hover.ContentFormat[0]
	}
	// Check if the client supports only line folding.

	if fr := caps.TextDocument.FoldingRange; fr != nil {
		o.LineFoldingOnly = fr.LineFoldingOnly
	}
	// Check if the client supports hierarchical document symbols.
	o.HierarchicalDocumentSymbolSupport = caps.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport

	// Client's 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 {
	// 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,
		Hooks: Hooks{
			GoDiff:               o.GoDiff,
			StaticcheckSupported: o.StaticcheckSupported,
			ComputeEdits:         o.ComputeEdits,
			GofumptFormat:        o.GofumptFormat,
			URLRegexp:            o.URLRegexp,
		},
		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)
	result.StandaloneTags = copySlice(o.StandaloneTags)

	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
}

func (o *Options) enableAllExperimentMaps() {
	if _, ok := o.Codelenses[string(command.GCDetails)]; !ok {
		o.Codelenses[string(command.GCDetails)] = true
	}
	if _, ok := o.Codelenses[string(command.RunGovulncheck)]; !ok {
		o.Codelenses[string(command.RunGovulncheck)] = true
	}
	if _, ok := o.Analyses[unusedparams.Analyzer.Name]; !ok {
		o.Analyses[unusedparams.Analyzer.Name] = true
	}
	if _, ok := o.Analyses[unusedvariable.Analyzer.Name]; !ok {
		o.Analyses[unusedvariable.Analyzer.Name] = true
	}
}

// validateDirectoryFilter validates if the filter string
// - is not empty
// - start with either + or -
// - doesn't contain currently unsupported glob operators: *, ?
func validateDirectoryFilter(ifilter string) (string, error) {
	filter := fmt.Sprint(ifilter)
	if filter == "" || (filter[0] != '+' && filter[0] != '-') {
		return "", fmt.Errorf("invalid filter %v, must start with + or -", filter)
	}
	segs := strings.Split(filter[1:], "/")
	unsupportedOps := [...]string{"?", "*"}
	for _, seg := range segs {
		if seg != "**" {
			for _, op := range unsupportedOps {
				if strings.Contains(seg, op) {
					return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op)
				}
			}
		}
	}

	return strings.TrimRight(filepath.FromSlash(filter), "/"), nil
}

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.parseErrorf("duplicate configuration for %s", name)
	}
	seen[name] = struct{}{}

	switch name {
	case "env":
		menv, ok := value.(map[string]interface{})
		if !ok {
			result.parseErrorf("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":
		// TODO(rfindley): use asStringSlice.
		iflags, ok := value.([]interface{})
		if !ok {
			result.parseErrorf("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":
		// TODO(rfindley): use asStringSlice.
		ifilters, ok := value.([]interface{})
		if !ok {
			result.parseErrorf("invalid type %T, expect list", value)
			break
		}
		var filters []string
		for _, ifilter := range ifilters {
			filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter))
			if err != nil {
				result.parseErrorf("%v", err)
				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 "symbolScope":
		if s, ok := result.asOneOf(
			string(WorkspaceSymbolScope),
			string(AllSymbolScope),
		); ok {
			o.SymbolScope = SymbolScope(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(BothShortcuts), string(LinkShortcut), string(DefinitionShortcut)); ok {
			o.ImportShortcut = ImportShortcut(s)
		}

	case "analyses":
		result.setBoolMap(&o.Analyses)

	case "hints":
		result.setBoolMap(&o.Hints)

	case "annotations":
		result.setAnnotationMap(&o.Annotations)

	case "vulncheck":
		if s, ok := result.asOneOf(
			string(ModeVulncheckOff),
			string(ModeVulncheckImports),
		); ok {
			o.Vulncheck = VulncheckMode(s)
		}

	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 {
				result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s;"+
					" rebuild 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":
		if v, ok := result.asBool(); ok {
			o.Gofumpt = v
			if v && o.GofumptFormat == nil {
				result.Error = fmt.Errorf("applying setting %q: gofumpt is not supported at %s;"+
					" rebuild gopls with a more recent version of Go", result.Name, runtime.Version())
			}
		}

	case "semanticTokens":
		result.setBool(&o.SemanticTokens)

	case "noSemanticString":
		result.setBool(&o.NoSemanticString)

	case "noSemanticNumber":
		result.setBool(&o.NoSemanticNumber)

	case "expandWorkspaceToModule":
		result.setBool(&o.ExpandWorkspaceToModule)

	case "experimentalPostfixCompletions":
		result.setBool(&o.ExperimentalPostfixCompletions)

	case "experimentalWorkspaceModule":
		result.deprecated("")

	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.parseErrorf("unexpected type %T not []string", value)

	case "experimentalDiagnosticsDelay":
		result.deprecated("diagnosticsDelay")

	case "diagnosticsDelay":
		result.setDuration(&o.DiagnosticsDelay)

	case "analysisProgressReporting":
		result.setBool(&o.AnalysisProgressReporting)

	case "experimentalWatchedFileDelay":
		result.deprecated("")

	case "experimentalPackageCacheKey":
		result.deprecated("")

	case "allowModfileModifications":
		result.setBool(&o.AllowModfileModifications)

	case "allowImplicitNetworkAccess":
		result.setBool(&o.AllowImplicitNetworkAccess)

	case "experimentalUseInvalidMetadata":
		result.deprecated("")

	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.

	case "newDiff":
		result.setString(&o.NewDiff)

	case "chattyDiagnostics":
		result.setBool(&o.ChattyDiagnostics)

	case "subdirWatchPatterns":
		if s, ok := result.asOneOf(
			string(SubdirWatchPatternsOn),
			string(SubdirWatchPatternsOff),
			string(SubdirWatchPatternsAuto),
		); ok {
			o.SubdirWatchPatterns = SubdirWatchPatterns(s)
		}

	case "reportAnalysisProgressAfter":
		result.setDuration(&o.ReportAnalysisProgressAfter)

	// 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
}

// parseErrorf reports an error parsing the current configuration value.
func (r *OptionResult) parseErrorf(msg string, values ...interface{}) {
	if false {
		_ = fmt.Sprintf(msg, values...) // this causes vet to check this like printf
	}
	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
}

// deprecated reports the current setting as deprecated. If 'replacement' is
// non-nil, it is suggested to the user.
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}
}

// unexpected reports that the current setting is not known to gopls.
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.parseErrorf("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.parseErrorf("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.parseErrorf(`"noEscape" is deprecated, set "Escape: false" instead`)
			case "noNilcheck":
				m[Nil] = false
				r.parseErrorf(`"noNilcheck" is deprecated, set "Nil: false" instead`)
			case "noInline":
				m[Inline] = false
				r.parseErrorf(`"noInline" is deprecated, set "Inline: false" instead`)
			case "noBounds":
				m[Bounds] = false
				r.parseErrorf(`"noBounds" is deprecated, set "Bounds: false" instead`)
			default:
				r.parseErrorf("%v", err)
			}
			continue
		}
		m[Annotation(a)] = enabled
	}
	*bm = m
}

func (r *OptionResult) asBoolMap() map[string]bool {
	all, ok := r.Value.(map[string]interface{})
	if !ok {
		r.parseErrorf("invalid type %T for map[string]bool option", r.Value)
		return nil
	}
	m := make(map[string]bool)
	for a, enabled := range all {
		if e, ok := enabled.(bool); ok {
			m[a] = e
		} else {
			r.parseErrorf("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.parseErrorf("invalid type %T, expect string", r.Value)
		return "", false
	}
	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 {
		return "", false
	}
	s, err := asOneOf(s, options...)
	if err != nil {
		r.parseErrorf("%v", err)
	}
	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
	}
}

func (r *OptionResult) setStringSlice(s *[]string) {
	if v, ok := r.asStringSlice(); ok {
		*s = v
	}
}

func typeErrorAnalyzers() map[string]*Analyzer {
	return map[string]*Analyzer{
		fillreturns.Analyzer.Name: {
			Analyzer: fillreturns.Analyzer,
			// TODO(rfindley): is SourceFixAll even necessary here? Is that not implied?
			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,
		},
		unusedvariable.Analyzer.Name: {
			Analyzer: unusedvariable.Analyzer,
			Enabled:  false,
		},
	}
}

// TODO(golang/go#61559): remove convenience analyzers now that they are not
// used from the analysis framework.
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,
			Fix:      StubMethods,
			Enabled:  true,
		},
		infertypeargs.Analyzer.Name: {
			Analyzer:   infertypeargs.Analyzer,
			Enabled:    true,
			ActionKind: []protocol.CodeActionKind{protocol.RefactorRewrite},
		},
	}
}

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},
		defers.Analyzer.Name:        {Analyzer: defers.Analyzer, Enabled: true},
		deprecated.Analyzer.Name:    {Analyzer: deprecated.Analyzer, Enabled: true, Severity: protocol.SeverityHint, Tag: []protocol.DiagnosticTag{protocol.Deprecated}},
		directive.Analyzer.Name:     {Analyzer: directive.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},
		slog.Analyzer.Name:          {Analyzer: slog.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},
		timeformat.Analyzer.Name:       {Analyzer: timeformat.Analyzer, Enabled: true},
		embeddirective.Analyzer.Name: {
			Analyzer:        embeddirective.Analyzer,
			Enabled:         true,
			Fix:             AddEmbedImport,
			fixesDiagnostic: fixedByImportingEmbed,
		},

		// 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
	URL     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)
}
