gopls/internal/lsp: add gopls.fetch_vulncheck_result
This CL changes the signature of View's Vulnerabilities
and SetVulnerabilities, to use govulncheck.Result
instead of []*govulncheck.Vuln. That allows us to hold
extra information about the analysis in addition to
the list of vulnerabilities.
Also, instead of aliasing x/vuln/exp/govulncheck.Result
define our own gopls/internal/govulncheck.Result type
so the gopls documentation is less confusing.
Change-Id: I7a18da9bf047b9ebed6fc0264b5e0f66c04ba3f3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/451696
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index c942be4..b6eee96 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -100,6 +100,26 @@
}
```
+### **Get known vulncheck result**
+Identifier: `gopls.fetch_vulncheck_result`
+
+Fetch the result of latest vulnerability check (`govulncheck`).
+
+Args:
+
+```
+{
+ // The file URI.
+ "URI": string,
+}
+```
+
+Result:
+
+```
+map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/govulncheck.Result
+```
+
### **Toggle gc_details**
Identifier: `gopls.gc_details`
diff --git a/gopls/internal/govulncheck/types.go b/gopls/internal/govulncheck/types.go
new file mode 100644
index 0000000..6cf0415
--- /dev/null
+++ b/gopls/internal/govulncheck/types.go
@@ -0,0 +1,12 @@
+// 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 govulncheck
+
+// Result is the result of vulnerability scanning.
+type Result struct {
+ // Vulns contains all vulnerabilities that are called or imported by
+ // the analyzed module.
+ Vulns []*Vuln
+}
diff --git a/gopls/internal/govulncheck/types_118.go b/gopls/internal/govulncheck/types_118.go
index 7410963..7b354d6 100644
--- a/gopls/internal/govulncheck/types_118.go
+++ b/gopls/internal/govulncheck/types_118.go
@@ -8,7 +8,9 @@
// Package govulncheck provides an experimental govulncheck API.
package govulncheck
-import "golang.org/x/vuln/exp/govulncheck"
+import (
+ "golang.org/x/vuln/exp/govulncheck"
+)
var (
// Source reports vulnerabilities that affect the analyzed packages.
@@ -22,9 +24,6 @@
// Config is the configuration for Main.
Config = govulncheck.Config
- // Result is the result of executing Source.
- Result = govulncheck.Result
-
// Vuln represents a single OSV entry.
Vuln = govulncheck.Vuln
diff --git a/gopls/internal/govulncheck/types_not118.go b/gopls/internal/govulncheck/types_not118.go
index ae92caa..faf5a70 100644
--- a/gopls/internal/govulncheck/types_not118.go
+++ b/gopls/internal/govulncheck/types_not118.go
@@ -13,13 +13,6 @@
"golang.org/x/vuln/osv"
)
-// Result is the result of executing Source or Binary.
-type Result struct {
- // Vulns contains all vulnerabilities that are called or imported by
- // the analyzed module.
- Vulns []*Vuln
-}
-
// Vuln represents a single OSV entry.
type Vuln struct {
// OSV contains all data from the OSV entry for this vulnerability.
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 9b43553..4916f42 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -239,7 +239,7 @@
name: name,
folder: folder,
moduleUpgrades: map[span.URI]map[string]string{},
- vulns: map[span.URI][]*govulncheck.Vuln{},
+ vulns: map[span.URI]*govulncheck.Result{},
filesByURI: map[span.URI]*fileBase{},
filesByBase: map[string][]*fileBase{},
rootURI: root,
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index f373c00..8198353 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -61,7 +61,7 @@
// Each modfile has a map of module name to upgrade version.
moduleUpgrades map[span.URI]map[string]string
- vulns map[span.URI][]*govulncheck.Vuln
+ vulns map[span.URI]*govulncheck.Result
// keep track of files by uri and by basename, a single file may be mapped
// to multiple uris, and the same basename may map to multiple files
@@ -1044,16 +1044,24 @@
delete(v.moduleUpgrades, modfile)
}
-func (v *View) Vulnerabilities(modfile span.URI) []*govulncheck.Vuln {
+func (v *View) Vulnerabilities(modfile ...span.URI) map[span.URI]*govulncheck.Result {
+ m := make(map[span.URI]*govulncheck.Result)
v.mu.Lock()
defer v.mu.Unlock()
- vulns := make([]*govulncheck.Vuln, len(v.vulns[modfile]))
- copy(vulns, v.vulns[modfile])
- return vulns
+ if len(modfile) == 0 {
+ for k, v := range v.vulns {
+ m[k] = v
+ }
+ return m
+ }
+ for _, f := range modfile {
+ m[f] = v.vulns[f]
+ }
+ return m
}
-func (v *View) SetVulnerabilities(modfile span.URI, vulns []*govulncheck.Vuln) {
+func (v *View) SetVulnerabilities(modfile span.URI, vulns *govulncheck.Result) {
v.mu.Lock()
defer v.mu.Unlock()
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index 28cf347..90b8408 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -833,6 +833,17 @@
Tests bool
}
+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 {
+ for modfile, result := range deps.snapshot.View().Vulnerabilities() {
+ ret[protocol.URIFromSpanURI(modfile)] = result
+ }
+ return nil
+ })
+ return ret, err
+}
+
func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) error {
if args.URI == "" {
return errors.New("VulncheckArgs is missing URI field")
@@ -887,10 +898,10 @@
// TODO: for easy debugging, log the failed stdout somewhere?
return fmt.Errorf("failed to parse govulncheck output: %v", err)
}
- vulns := result.Vulns
- deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns)
- c.s.diagnoseSnapshot(deps.snapshot, nil, false)
+ deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), &result)
+ c.s.diagnoseSnapshot(deps.snapshot, nil, false)
+ vulns := result.Vulns
affecting := make([]string, 0, len(vulns))
for _, v := range vulns {
if v.IsCalled() {
diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go
index 615f950..5111e67 100644
--- a/gopls/internal/lsp/command/command_gen.go
+++ b/gopls/internal/lsp/command/command_gen.go
@@ -24,6 +24,7 @@
ApplyFix Command = "apply_fix"
CheckUpgrades Command = "check_upgrades"
EditGoDirective Command = "edit_go_directive"
+ FetchVulncheckResult Command = "fetch_vulncheck_result"
GCDetails Command = "gc_details"
Generate Command = "generate"
GenerateGoplsMod Command = "generate_gopls_mod"
@@ -50,6 +51,7 @@
ApplyFix,
CheckUpgrades,
EditGoDirective,
+ FetchVulncheckResult,
GCDetails,
Generate,
GenerateGoplsMod,
@@ -102,6 +104,12 @@
return nil, err
}
return nil, s.EditGoDirective(ctx, a0)
+ case "gopls.fetch_vulncheck_result":
+ var a0 URIArg
+ if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+ return nil, err
+ }
+ return s.FetchVulncheckResult(ctx, a0)
case "gopls.gc_details":
var a0 protocol.DocumentURI
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -276,6 +284,18 @@
}, nil
}
+func NewFetchVulncheckResultCommand(title string, a0 URIArg) (protocol.Command, error) {
+ args, err := MarshalArgs(a0)
+ if err != nil {
+ return protocol.Command{}, err
+ }
+ return protocol.Command{
+ Title: title,
+ Command: "gopls.fetch_vulncheck_result",
+ Arguments: args,
+ }, nil
+}
+
func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {
diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go
index 23b9f65..e6ab13d 100644
--- a/gopls/internal/lsp/command/interface.go
+++ b/gopls/internal/lsp/command/interface.go
@@ -17,6 +17,7 @@
import (
"context"
+ "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/protocol"
)
@@ -153,6 +154,11 @@
//
// Run vulnerability check (`govulncheck`).
RunVulncheckExp(context.Context, VulncheckArgs) error
+
+ // FetchVulncheckResult: Get known vulncheck result
+ //
+ // Fetch the result of latest vulnerability check (`govulncheck`).
+ FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error)
}
type RunTestsArgs struct {
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index 3b74bd8..ec0993f 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -180,14 +180,17 @@
return nil, err
}
- vs := snapshot.View().Vulnerabilities(fh.URI())
- // TODO(suzmue): should we just store the vulnerabilities like this?
+ vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()]
+ if vs == nil || len(vs.Vulns) == 0 {
+ return nil, nil
+ }
+
type modVuln struct {
mod *govulncheck.Module
vuln *govulncheck.Vuln
}
vulnsByModule := make(map[string][]modVuln)
- for _, vuln := range vs {
+ for _, vuln := range vs.Vulns {
for _, mod := range vuln.Modules {
vulnsByModule[mod.Path] = append(vulnsByModule[mod.Path], modVuln{mod, vuln})
}
diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go
index 520a029..7706262 100644
--- a/gopls/internal/lsp/mod/hover.go
+++ b/gopls/internal/lsp/mod/hover.go
@@ -71,7 +71,7 @@
}
// Get the vulnerability info.
- affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req.Mod.Path)
+ affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI())[fh.URI()], req.Mod.Path)
// Get the `go mod why` results for the given file.
why, err := snapshot.ModWhy(ctx, fh)
@@ -117,8 +117,11 @@
return b.String()
}
-func lookupVulns(vulns []*govulncheck.Vuln, modpath string) (affecting, nonaffecting []*govulncheck.Vuln) {
- for _, vuln := range vulns {
+func lookupVulns(vulns *govulncheck.Result, modpath string) (affecting, nonaffecting []*govulncheck.Vuln) {
+ if vulns == nil {
+ return nil, nil
+ }
+ for _, vuln := range vulns.Vulns {
for _, mod := range vuln.Modules {
if mod.Path != modpath {
continue
diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go
index e283c3b..fdc4648 100755
--- a/gopls/internal/lsp/source/api_json.go
+++ b/gopls/internal/lsp/source/api_json.go
@@ -690,6 +690,13 @@
ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The version to pass to `go mod edit -go`.\n\t\"Version\": string,\n}",
},
{
+ Command: "gopls.fetch_vulncheck_result",
+ Title: "Get known vulncheck result",
+ Doc: "Fetch the result of latest vulnerability check (`govulncheck`).",
+ ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}",
+ ResultDoc: "map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/govulncheck.Result",
+ },
+ {
Command: "gopls.gc_details",
Title: "Toggle gc_details",
Doc: "Toggle the calculation of gc annotations.",
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 7466484..aed3f54 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -314,11 +314,11 @@
// Vulnerabilites returns known vulnerabilities for the given modfile.
// TODO(suzmue): replace command.Vuln with a different type, maybe
// https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary?
- Vulnerabilities(modfile span.URI) []*govulncheck.Vuln
+ Vulnerabilities(modfile ...span.URI) map[span.URI]*govulncheck.Result
// SetVulnerabilities resets the list of vulnerabilites that exists for the given modules
// required by modfile.
- SetVulnerabilities(modfile span.URI, vulnerabilities []*govulncheck.Vuln)
+ SetVulnerabilities(modfile span.URI, vulncheckResult *govulncheck.Result)
// FileKind returns the type of a file
FileKind(FileHandle) FileKind
diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go
index b511eb7..a9587c2 100644
--- a/gopls/internal/regtest/misc/vuln_test.go
+++ b/gopls/internal/regtest/misc/vuln_test.go
@@ -15,6 +15,7 @@
"testing"
"github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/protocol"
. "golang.org/x/tools/gopls/internal/lsp/regtest"
@@ -180,9 +181,43 @@
// TODO(hyangah): once the diagnostics are published, wait for diagnostics.
ShownMessage("Found GOSTDLIB"),
)
+ testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GOSTDLIB"}})
})
}
+func testFetchVulncheckResult(t *testing.T, env *Env, want map[string][]string) {
+ t.Helper()
+
+ var result map[protocol.DocumentURI]*govulncheck.Result
+ fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{
+ URI: env.Sandbox.Workdir.URI("go.mod"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ env.ExecuteCommand(&protocol.ExecuteCommandParams{
+ Command: fetchCmd.Command,
+ Arguments: fetchCmd.Arguments,
+ }, &result)
+
+ for _, v := range want {
+ sort.Strings(v)
+ }
+ got := map[string][]string{}
+ for k, r := range result {
+ var osv []string
+ for _, v := range r.Vulns {
+ osv = append(osv, v.OSV.ID)
+ }
+ sort.Strings(osv)
+ modfile := env.Sandbox.Workdir.RelPath(k.SpanURI().Filename())
+ got[modfile] = osv
+ }
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff)
+ }
+}
+
const workspace1 = `
-- go.mod --
module golang.org/entry
@@ -332,6 +367,9 @@
ReadDiagnostics("go.mod", gotDiagnostics),
),
)
+ testFetchVulncheckResult(t, env, map[string][]string{
+ "go.mod": {"GO-2022-01", "GO-2022-02", "GO-2022-03"},
+ })
env.OpenFile("x/x.go")
lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`)
env.OpenFile("y/y.go")
@@ -470,6 +508,7 @@
ReadDiagnostics("go.mod", gotDiagnostics)),
ShownMessage("No vulnerabilities found")) // only count affecting vulnerabilities.
+ testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GO-2022-02"}})
// wantDiagnostics maps a module path in the require
// section of a go.mod to diagnostics that will be returned
// when running vulncheck.