internal/lsp/mod/code_lens: add "run govulncheck" codelens

And, make gopls.run_vulncheck_exp show an information/error
message popup after a successful run. This is temporary.
We plan to publish the results as diagnostics and quick-fix.

Finally, changed the stdlib vulnerability info id in
testdata to GO-0000-0001 which looks more like a vulnerability
ID than STD.

Changed TestRunVulncheckExp to include tests on codelens
and use the command included in the codelens, instead of
directly calling the gopls.run_vulncheck_exp command.

Change-Id: Iaf91e4e61b2dfc1e050b887946a69efd3e3785b0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/420995
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 9bf6e07..890a0a3 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -503,6 +503,11 @@
 Identifier: `regenerate_cgo`
 
 Regenerates cgo definitions.
+### **Run vulncheck (experimental)**
+
+Identifier: `run_vulncheck_exp`
+
+Run vulnerability check (`govulncheck`).
 ### **Run test(s) (legacy)**
 
 Identifier: `test`
diff --git a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json
index 240ee49..7cbfafc 100644
--- a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json
+++ b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json
@@ -1 +1 @@
-[{"id":"STD","affected":[{"package":{"name":"archive/zip"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"symbols":["OpenReader"]}}]}]
+[{"id":"GO-0000-001","affected":[{"package":{"name":"archive/zip"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"symbols":["OpenReader"]}}]}]
diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go
index 9de68b6..78c193e 100644
--- a/gopls/internal/regtest/misc/vuln_test.go
+++ b/gopls/internal/regtest/misc/vuln_test.go
@@ -61,7 +61,7 @@
 )
 
 func main() {
-        _, err := zip.OpenReader("file.zip")  // vulnerable.
+        _, err := zip.OpenReader("file.zip")  // vulnerability GO-0000-001
         fmt.Println(err)
 }
 `
@@ -79,22 +79,39 @@
 			"GOVERSION":                       "go1.18",
 			"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true",
 		},
+		Settings{
+			"codelenses": map[string]bool{
+				"run_vulncheck_exp": true,
+			},
+		},
 	).Run(t, files, func(t *testing.T, env *Env) {
-		cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{
-			URI:     protocol.URIFromPath(env.Sandbox.Workdir.AbsPath("go.mod")),
-			Pattern: "./...",
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
+		env.OpenFile("go.mod")
 
+		// Test CodeLens is present.
+		lenses := env.CodeLens("go.mod")
+
+		const wantCommand = "gopls." + string(command.RunVulncheckExp)
+		var gotCodelens = false
+		var lens protocol.CodeLens
+		for _, l := range lenses {
+			if l.Command.Command == wantCommand {
+				gotCodelens = true
+				lens = l
+				break
+			}
+		}
+		if !gotCodelens {
+			t.Fatal("got no vulncheck codelens")
+		}
+		// Run Command included in the codelens.
 		env.ExecuteCommand(&protocol.ExecuteCommandParams{
-			Command:   command.RunVulncheckExp.ID(),
-			Arguments: cmd.Arguments,
+			Command:   lens.Command.Command,
+			Arguments: lens.Command.Arguments,
 		}, nil)
 		env.Await(
 			CompletedWork("Checking vulnerability", 1, true),
 			// TODO(hyangah): once the diagnostics are published, wait for diagnostics.
+			ShownMessage("Found GO-0000-001"),
 		)
 	})
 }
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 7348c17..6df909b 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -823,8 +823,6 @@
 		}
 
 		cmd := exec.Command(os.Args[0], "vulncheck", "-config", args.Pattern)
-		// TODO(hyangah): if args.URI is not go.mod file, we need to
-		// adjust the directory accordingly.
 		cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename())
 
 		var viewEnv []string
@@ -860,8 +858,32 @@
 			return fmt.Errorf("failed to parse govulncheck output: %v", err)
 		}
 
-		// TODO(hyangah): convert the results to diagnostics & code actions.
-		return nil
+		// TODO(jamalc,suzmue): convert the results to diagnostics & code actions.
+		// Or should we just write to a file (*.vulncheck.json) or text format
+		// and send "Show Document" request? If *.vulncheck.json is open,
+		// VSCode Go extension will open its custom editor.
+		set := make(map[string]bool)
+		for _, v := range vulns.Vuln {
+			if len(v.CallStackSummaries) > 0 {
+				set[v.ID] = true
+			}
+		}
+		if len(set) == 0 {
+			return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+				Type:    protocol.Info,
+				Message: "No vulnerabilities found",
+			})
+		}
+
+		list := make([]string, 0, len(set))
+		for k := range set {
+			list = append(list, k)
+		}
+		sort.Strings(list)
+		return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+			Type:    protocol.Warning,
+			Message: fmt.Sprintf("Found %v", strings.Join(list, ", ")),
+		})
 	})
 	return err
 }
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index b26bae7..1de25c2 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -22,6 +22,7 @@
 		command.UpgradeDependency: upgradeLenses,
 		command.Tidy:              tidyLens,
 		command.Vendor:            vendorLens,
+		command.RunVulncheckExp:   vulncheckLenses,
 	}
 }
 
@@ -151,3 +152,29 @@
 	}
 	return source.LineToRange(pm.Mapper, fh.URI(), start, end)
 }
+
+func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
+	pm, err := snapshot.ParseMod(ctx, fh)
+	if err != nil || pm.File == nil {
+		return nil, err
+	}
+	// Place the codelenses near the module statement.
+	// A module may not have the require block,
+	// but vulnerabilities can exist in standard libraries.
+	uri := protocol.URIFromSpanURI(fh.URI())
+	rng, err := moduleStmtRange(fh, pm)
+	if err != nil {
+		return nil, err
+	}
+
+	vulncheck, err := command.NewRunVulncheckExpCommand("Run govulncheck", command.VulncheckArgs{
+		URI:     uri,
+		Pattern: "./...",
+	})
+	if err != nil {
+		return nil, err
+	}
+	return []protocol.CodeLens{
+		{Range: rng, Command: vulncheck},
+	}, nil
+}
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index cf75792..0b3b3d1 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -583,6 +583,11 @@
 							Default: "true",
 						},
 						{
+							Name:    "\"run_vulncheck_exp\"",
+							Doc:     "Run vulnerability check (`govulncheck`).",
+							Default: "false",
+						},
+						{
 							Name:    "\"test\"",
 							Doc:     "Runs `go test` for a specific set of test or benchmark functions.",
 							Default: "false",
@@ -808,6 +813,11 @@
 			Doc:   "Regenerates cgo definitions.",
 		},
 		{
+			Lens:  "run_vulncheck_exp",
+			Title: "Run vulncheck (experimental)",
+			Doc:   "Run vulnerability check (`govulncheck`).",
+		},
+		{
 			Lens:  "test",
 			Title: "Run test(s) (legacy)",
 			Doc:   "Runs `go test` for a specific set of test or benchmark functions.",
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 126752b..2f40b59 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -155,6 +155,7 @@
 						string(command.GCDetails):         false,
 						string(command.UpgradeDependency): true,
 						string(command.Vendor):            true,
+						// TODO(hyangah): enable command.RunVulncheckExp.
 					},
 				},
 			},