gopls/internal/lsp/cache: wire in mod vulnerability analysis

Add a stub implementation of incremental go.mod vulnerability analysis,
following the pattern of mod tidy analysis.

Change-Id: Iaaea71f6a8a7e735f8b595289a528e78b19e2560
Reviewed-on: https://go-review.googlesource.com/c/tools/+/452770
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/lsp/cache/mod_vuln.go
new file mode 100644
index 0000000..d783f5d
--- /dev/null
+++ b/gopls/internal/lsp/cache/mod_vuln.go
@@ -0,0 +1,72 @@
+// 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 cache
+
+import (
+	"context"
+	"os"
+
+	"golang.org/x/tools/gopls/internal/govulncheck"
+	"golang.org/x/tools/gopls/internal/lsp/source"
+	"golang.org/x/tools/gopls/internal/span"
+	"golang.org/x/tools/gopls/internal/vulncheck"
+	"golang.org/x/tools/internal/memoize"
+)
+
+// ModVuln returns import vulnerability analysis for the given go.mod URI.
+// Concurrent requests are combined into a single command.
+func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.Result, error) {
+	s.mu.Lock()
+	entry, hit := s.modVulnHandles.Get(modURI)
+	s.mu.Unlock()
+
+	type modVuln struct {
+		result *govulncheck.Result
+		err    error
+	}
+
+	// Cache miss?
+	if !hit {
+		// If the file handle is an overlay, it may not be written to disk.
+		// The go.mod file has to be on disk for vulncheck to work.
+		//
+		// TODO(hyangah): use overlays for vulncheck.
+		fh, err := s.GetFile(ctx, modURI)
+		if err != nil {
+			return nil, err
+		}
+		if _, ok := fh.(*overlay); ok {
+			if info, _ := os.Stat(modURI.Filename()); info == nil {
+				return nil, source.ErrNoModOnDisk
+			}
+		}
+
+		handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} {
+			result, err := modVulnImpl(ctx, arg.(*snapshot), modURI)
+			return modVuln{result, err}
+		})
+
+		entry = handle
+		s.mu.Lock()
+		s.modVulnHandles.Set(modURI, entry, nil)
+		s.mu.Unlock()
+	}
+
+	// Await result.
+	v, err := s.awaitPromise(ctx, entry.(*memoize.Promise))
+	if err != nil {
+		return nil, err
+	}
+	res := v.(modVuln)
+	return res.result, res.err
+}
+
+func modVulnImpl(ctx context.Context, s *snapshot, uri span.URI) (*govulncheck.Result, error) {
+	fh, err := s.GetFile(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+	return vulncheck.AnalyzeVulnerableImports(ctx, s, fh)
+}
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 4916f42..28acd5d 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -283,6 +283,7 @@
 		parseModHandles:      persistent.NewMap(uriLessInterface),
 		parseWorkHandles:     persistent.NewMap(uriLessInterface),
 		modTidyHandles:       persistent.NewMap(uriLessInterface),
+		modVulnHandles:       persistent.NewMap(uriLessInterface),
 		modWhyHandles:        persistent.NewMap(uriLessInterface),
 		knownSubdirs:         newKnownDirsSet(),
 		workspace:            workspace,
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index c2cb946..9a4c5b3 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -140,6 +140,7 @@
 	// the view's go.mod file.
 	modTidyHandles *persistent.Map // from span.URI to *memoize.Promise[modTidyResult]
 	modWhyHandles  *persistent.Map // from span.URI to *memoize.Promise[modWhyResult]
+	modVulnHandles *persistent.Map // from span.URI to *memoize.Promise[modVulnResult]
 
 	workspace *workspace // (not guarded by mu)
 
@@ -230,6 +231,7 @@
 	s.parseModHandles.Destroy()
 	s.parseWorkHandles.Destroy()
 	s.modTidyHandles.Destroy()
+	s.modVulnHandles.Destroy()
 	s.modWhyHandles.Destroy()
 
 	if s.workspaceDir != "" {
@@ -1713,6 +1715,7 @@
 		parseWorkHandles:     s.parseWorkHandles.Clone(),
 		modTidyHandles:       s.modTidyHandles.Clone(),
 		modWhyHandles:        s.modWhyHandles.Clone(),
+		modVulnHandles:       s.modVulnHandles.Clone(),
 		knownSubdirs:         s.knownSubdirs.Clone(),
 		workspace:            newWorkspace,
 	}
@@ -1752,6 +1755,7 @@
 		// Invalidate go.mod-related handles.
 		result.modTidyHandles.Delete(uri)
 		result.modWhyHandles.Delete(uri)
+		result.modVulnHandles.Delete(uri)
 
 		// Invalidate handles for cached symbols.
 		result.symbolizeHandles.Delete(uri)
@@ -1815,9 +1819,9 @@
 			// TODO(maybe): Only delete mod handles for
 			// which the withoutURI is relevant.
 			// Requires reverse-engineering the go command. (!)
-
 			result.modTidyHandles.Clear()
 			result.modWhyHandles.Clear()
+			result.modVulnHandles.Clear()
 		}
 
 		result.parseModHandles.Delete(uri)