gopls/internal/lsp: include all vulns info to fetch_vulncheck_result

In addition to the govulncheck analysis result, gopls now has its own
vulnerability scanning that reports vulnerabilities on known packages
in the workspace. While we are still debating on the right name for
this analysis mode, let's call it 'light-weight' vuln scanning because
this is less expensive than the govulncheck analysis (but with less
precise result).

fetch_vulncheck_result now includes the vulnerability analysis results
from the light-weight vuln scanning. We distinguish the analysis source
by adding Analysis field in the govulncheck.Result type.

For each go.mod, if govulncheck result exists, return it.
Otherwise, return the light-weight vuln scanning result if it is
available in the snapshot.

Change-Id: I8d53db474137cecc1138ae432207bf508c09ca6f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/453195
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/govulncheck/types.go b/gopls/internal/govulncheck/types.go
index 6cf0415..75240aa 100644
--- a/gopls/internal/govulncheck/types.go
+++ b/gopls/internal/govulncheck/types.go
@@ -9,4 +9,23 @@
 	// Vulns contains all vulnerabilities that are called or imported by
 	// the analyzed module.
 	Vulns []*Vuln
+
+	// Mode contains the source of the vulnerability info.
+	// Clients of the gopls.fetch_vulncheck_result command may need
+	// to interprete the vulnerabilities differently based on the
+	// analysis mode. For example, Vuln without callstack traces
+	// indicate a vulnerability that is not used if the result was
+	// from 'govulncheck' analysis mode. On the other hand, Vuln
+	// without callstack traces just implies the package with the
+	// vulnerability is known to the workspace and we do not know
+	// whether the vulnerable symbols are actually used or not.
+	Mode AnalysisMode
 }
+
+type AnalysisMode string
+
+const (
+	ModeInvalid     AnalysisMode = "" // zero value
+	ModeGovulncheck AnalysisMode = "govulncheck"
+	ModeImports     AnalysisMode = "imports"
+)
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index 4a53772..7f3652d 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -843,6 +843,16 @@
 func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error) {
 	ret := map[protocol.DocumentURI]*govulncheck.Result{}
 	err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error {
+		if deps.snapshot.View().Options().Vulncheck == source.ModeVulncheckImports {
+			for _, modfile := range deps.snapshot.ModFiles() {
+				res, err := deps.snapshot.ModVuln(ctx, modfile)
+				if err != nil {
+					return err
+				}
+				ret[protocol.URIFromSpanURI(modfile)] = res
+			}
+		}
+		// Overwrite if there is any govulncheck-based result.
 		for modfile, result := range deps.snapshot.View().Vulnerabilities() {
 			ret[protocol.URIFromSpanURI(modfile)] = result
 		}
@@ -914,6 +924,7 @@
 			// TODO: for easy debugging, log the failed stdout somewhere?
 			return fmt.Errorf("failed to parse govulncheck output: %v", err)
 		}
+		result.Mode = govulncheck.ModeGovulncheck
 		deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), &result)
 
 		c.s.diagnoseSnapshot(deps.snapshot, nil, false)
diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go
index c37d404..734a873 100644
--- a/gopls/internal/regtest/misc/vuln_test.go
+++ b/gopls/internal/regtest/misc/vuln_test.go
@@ -197,11 +197,17 @@
 				ShownMessage("Found GOSTDLIB"),
 			),
 		)
-		testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GOSTDLIB"}})
+		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
+			"go.mod": {IDs: []string{"GOSTDLIB"}, Mode: govulncheck.ModeGovulncheck}})
 	})
 }
 
-func testFetchVulncheckResult(t *testing.T, env *Env, want map[string][]string) {
+type fetchVulncheckResult struct {
+	IDs  []string
+	Mode govulncheck.AnalysisMode
+}
+
+func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulncheckResult) {
 	t.Helper()
 
 	var result map[protocol.DocumentURI]*govulncheck.Result
@@ -217,9 +223,9 @@
 	}, &result)
 
 	for _, v := range want {
-		sort.Strings(v)
+		sort.Strings(v.IDs)
 	}
-	got := map[string][]string{}
+	got := map[string]fetchVulncheckResult{}
 	for k, r := range result {
 		var osv []string
 		for _, v := range r.Vulns {
@@ -227,7 +233,10 @@
 		}
 		sort.Strings(osv)
 		modfile := env.Sandbox.Workdir.RelPath(k.SpanURI().Filename())
-		got[modfile] = osv
+		got[modfile] = fetchVulncheckResult{
+			IDs:  osv,
+			Mode: r.Mode,
+		}
 	}
 	if diff := cmp.Diff(want, got); diff != "" {
 		t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff)
@@ -396,7 +405,12 @@
 			ReadDiagnostics("go.mod", gotDiagnostics),
 		)
 
-		testFetchVulncheckResult(t, env, map[string][]string{})
+		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
+			"go.mod": {
+				IDs:  []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"},
+				Mode: govulncheck.ModeImports,
+			},
+		})
 
 		wantVulncheckDiagnostics := map[string]vulnDiagExpectation{
 			"golang.org/amod": {
@@ -449,7 +463,7 @@
 		if len(gotDiagnostics.Diagnostics) > 0 {
 			t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics))
 		}
-		testFetchVulncheckResult(t, env, map[string][]string{})
+		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{})
 	}
 
 	for _, tc := range []struct {
@@ -510,8 +524,8 @@
 			),
 		)
 
-		testFetchVulncheckResult(t, env, map[string][]string{
-			"go.mod": {"GO-2022-01", "GO-2022-02", "GO-2022-03"},
+		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
+			"go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: govulncheck.ModeGovulncheck},
 		})
 		env.OpenFile("x/x.go")
 		lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`)
@@ -672,7 +686,7 @@
 			),
 		)
 
-		testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GO-2022-02"}})
+		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: govulncheck.ModeGovulncheck}})
 		// wantDiagnostics maps a module path in the require
 		// section of a go.mod to diagnostics that will be returned
 		// when running vulncheck.
@@ -791,7 +805,7 @@
 	return i.Message < j.Message
 }
 
-// wantVulncheckModDiagnostics maps a module path in the require
+// vulnDiagExpectation maps a module path in the require
 // section of a go.mod to diagnostics that will be returned
 // when running vulncheck.
 type vulnDiagExpectation struct {
diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go
index 86db0a3..d29318e 100644
--- a/gopls/internal/vulncheck/command.go
+++ b/gopls/internal/vulncheck/command.go
@@ -476,6 +476,7 @@
 	})
 	ret := &govulncheck.Result{
 		Vulns: vulns,
+		Mode:  govulncheck.ModeImports,
 	}
 	return ret, nil
 }