internal/lsp/command: add RunVulncheckExp

This is a command that runs govulncheck-like analysis.
This is highly experimental and can change any time,
so we mark it with the "Exp" suffix. Once the interface
becomes stable, we will rename this command.

It returns VulncheckResult that can be encoded as
a JSON message. The result includes all potentially
affecting vulnerabilities, and sample traces.

This feature is currently available only when gopls
is compiled with go1.18. Otherwise, the command will
return an error.

Updates golang/go#50577
Updates golang/vscode-go#2096

Change-Id: Ia37b0555f7bf98760292c9f68e50fb70dd494522
Reviewed-on: https://go-review.googlesource.com/c/tools/+/395576
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 37a4ca4..b4b16dd 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -265,6 +265,41 @@
 }
 ```
 
+### **Run vulncheck (experimental)**
+Identifier: `gopls.run_vulncheck_exp`
+
+Run vulnerability check (`govulncheck`).
+
+Args:
+
+```
+{
+	// Dir is the directory from which vulncheck will run from.
+	"Dir": string,
+	// Package pattern. E.g. "", ".", "./...".
+	"Pattern": string,
+}
+```
+
+Result:
+
+```
+{
+	"Vuln": []{
+		"id": string,
+		"details": string,
+		"aliases": []string,
+		"symbol": string,
+		"pkg_path": string,
+		"mod_path": string,
+		"url": string,
+		"current_version": string,
+		"fixed_version": string,
+		"call_stacks": [][]golang.org/x/tools/internal/lsp/command.StackEntry,
+	},
+}
+```
+
 ### **Start the gopls debug server**
 Identifier: `gopls.start_debugging`
 
diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go
index db554f5..023aefe 100644
--- a/gopls/internal/hooks/hooks.go
+++ b/gopls/internal/hooks/hooks.go
@@ -10,6 +10,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/vulncheck"
 	"golang.org/x/tools/internal/lsp/source"
 	"mvdan.cc/gofumpt/format"
 	"mvdan.cc/xurls/v2"
@@ -28,4 +29,6 @@
 		})
 	}
 	updateAnalyzers(options)
+
+	options.Govulncheck = vulncheck.Govulncheck
 }
diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go
index efc2ceb..a367b06 100644
--- a/gopls/internal/vulncheck/command.go
+++ b/gopls/internal/vulncheck/command.go
@@ -5,15 +5,13 @@
 //go:build go1.18
 // +build go1.18
 
-// Package vulncheck provides an analysis command
-// that runs vulnerability analysis using data from
-// golang.org/x/exp/vulncheck.
-// This package requires go1.18 or newer.
 package vulncheck
 
 import (
 	"context"
 	"fmt"
+	"os"
+	"strings"
 
 	"golang.org/x/exp/vulncheck"
 	"golang.org/x/tools/go/packages"
@@ -21,6 +19,42 @@
 	"golang.org/x/vuln/client"
 )
 
+func init() {
+	Govulncheck = govulncheck
+}
+
+func govulncheck(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) {
+	if args.Pattern == "" {
+		args.Pattern = "."
+	}
+
+	dbClient, err := client.NewClient(findGOVULNDB(cfg), client.Options{HTTPCache: defaultCache()})
+	if err != nil {
+		return res, err
+	}
+
+	c := cmd{Client: dbClient}
+	vulns, err := c.Run(ctx, cfg, args.Pattern)
+	if err != nil {
+		return res, err
+	}
+
+	res.Vuln = vulns
+	return res, err
+}
+
+func findGOVULNDB(cfg *packages.Config) []string {
+	for _, kv := range cfg.Env {
+		if strings.HasPrefix(kv, "GOVULNDB=") {
+			return strings.Split(kv[len("GOVULNDB="):], ",")
+		}
+	}
+	if GOVULNDB := os.Getenv("GOVULNDB"); GOVULNDB != "" {
+		return strings.Split(GOVULNDB, ",")
+	}
+	return []string{"https://storage.googleapis.com/go-vulndb"}
+}
+
 type Vuln = command.Vuln
 type CallStack = command.CallStack
 type StackEntry = command.StackEntry
diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go
new file mode 100644
index 0000000..198f974
--- /dev/null
+++ b/gopls/internal/vulncheck/vulncheck.go
@@ -0,0 +1,23 @@
+// Copyright 2022 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 vulncheck provides an analysis command
+// that runs vulnerability analysis using data from
+// golang.org/x/exp/vulncheck.
+// This package requires go1.18 or newer.
+package vulncheck
+
+import (
+	"context"
+	"errors"
+
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/lsp/command"
+)
+
+// Govulncheck runs the in-process govulncheck implementation.
+// With go1.18+, this is swapped with the real implementation.
+var Govulncheck = func(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) {
+	return res, errors.New("not implemented")
+}
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 6f491f7..de8d88c 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -18,6 +18,7 @@
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/lsp/command"
@@ -781,3 +782,35 @@
 	result.URLs = []string{"http://" + listenedAddr}
 	return result, nil
 }
+
+func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) (result command.VulncheckResult, _ error) {
+	err := c.run(ctx, commandConfig{
+		progress:    "Running vulncheck",
+		requireSave: true,
+		forURI:      args.Dir, // Will dir work?
+	}, func(ctx context.Context, deps commandDeps) error {
+		view := deps.snapshot.View()
+		opts := view.Options()
+		if opts == nil || opts.Hooks.Govulncheck == nil {
+			return errors.New("vulncheck feature is not available")
+		}
+
+		buildFlags := opts.BuildFlags // XXX: is session.Options equivalent to view.Options?
+		var viewEnv []string
+		if e := opts.EnvSlice(); e != nil {
+			viewEnv = append(os.Environ(), e...)
+		}
+		cfg := &packages.Config{
+			Context:    ctx,
+			Tests:      true, // TODO(hyangah): add a field in args.
+			BuildFlags: buildFlags,
+			Env:        viewEnv,
+			Dir:        view.Folder().Filename(),
+			// TODO(hyangah): configure overlay
+		}
+		var err error
+		result, err = opts.Hooks.Govulncheck(ctx, cfg, args)
+		return err
+	})
+	return result, err
+}
diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go
index 5569693..22cfeff 100644
--- a/internal/lsp/command/command_gen.go
+++ b/internal/lsp/command/command_gen.go
@@ -33,6 +33,7 @@
 	RegenerateCgo     Command = "regenerate_cgo"
 	RemoveDependency  Command = "remove_dependency"
 	RunTests          Command = "run_tests"
+	RunVulncheckExp   Command = "run_vulncheck_exp"
 	StartDebugging    Command = "start_debugging"
 	Test              Command = "test"
 	Tidy              Command = "tidy"
@@ -57,6 +58,7 @@
 	RegenerateCgo,
 	RemoveDependency,
 	RunTests,
+	RunVulncheckExp,
 	StartDebugging,
 	Test,
 	Tidy,
@@ -152,6 +154,12 @@
 			return nil, err
 		}
 		return nil, s.RunTests(ctx, a0)
+	case "gopls.run_vulncheck_exp":
+		var a0 VulncheckArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		return s.RunVulncheckExp(ctx, a0)
 	case "gopls.start_debugging":
 		var a0 DebuggingArgs
 		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -368,6 +376,18 @@
 	}, nil
 }
 
+func NewRunVulncheckExpCommand(title string, a0 VulncheckArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.run_vulncheck_exp",
+		Arguments: args,
+	}, nil
+}
+
 func NewStartDebuggingCommand(title string, a0 DebuggingArgs) (protocol.Command, error) {
 	args, err := MarshalArgs(a0)
 	if err != nil {
diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go
index cd060fd..8985adb 100644
--- a/internal/lsp/command/interface.go
+++ b/internal/lsp/command/interface.go
@@ -143,6 +143,11 @@
 	// Start the gopls debug server if it isn't running, and return the debug
 	// address.
 	StartDebugging(context.Context, DebuggingArgs) (DebuggingResult, error)
+
+	// RunVulncheckExp: Run vulncheck (experimental)
+	//
+	// Run vulnerability check (`govulncheck`).
+	RunVulncheckExp(context.Context, VulncheckArgs) (VulncheckResult, error)
 }
 
 type RunTestsArgs struct {
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index 5e7b440..1db1700 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -666,6 +666,13 @@
 			ArgDoc:  "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}",
 		},
 		{
+			Command:   "gopls.run_vulncheck_exp",
+			Title:     "Run vulncheck (experimental)",
+			Doc:       "Run vulnerability check (`govulncheck`).",
+			ArgDoc:    "{\n\t// Dir is the directory from which vulncheck will run from.\n\t\"Dir\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}",
+			ResultDoc: "{\n\t\"Vuln\": []{\n\t\t\"id\": string,\n\t\t\"details\": string,\n\t\t\"aliases\": []string,\n\t\t\"symbol\": string,\n\t\t\"pkg_path\": string,\n\t\t\"mod_path\": string,\n\t\t\"url\": string,\n\t\t\"current_version\": string,\n\t\t\"fixed_version\": string,\n\t\t\"call_stacks\": [][]golang.org/x/tools/internal/lsp/command.StackEntry,\n\t},\n}",
+		},
+		{
 			Command:   "gopls.start_debugging",
 			Title:     "Start the gopls debug server",
 			Doc:       "Start the gopls debug server if it isn't running, and return the debug\naddress.",
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index b3b3ef2..319f77e 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -47,6 +47,7 @@
 	"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/fillreturns"
 	"golang.org/x/tools/internal/lsp/analysis/fillstruct"
 	"golang.org/x/tools/internal/lsp/analysis/infertypeargs"
@@ -476,6 +477,9 @@
 	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
@@ -703,6 +707,7 @@
 			ComputeEdits:  o.ComputeEdits,
 			GofumptFormat: o.GofumptFormat,
 			URLRegexp:     o.URLRegexp,
+			Govulncheck:   o.Govulncheck,
 		},
 		ServerOptions: o.ServerOptions,
 		UserOptions:   o.UserOptions,