Skip to content

Commit 87ac91f

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/server: revert the gopls.run_govulncheck command
As described in golang/vscode-go#3572 this CL reverts the behavior of the gopls.run_govulncheck command to be asynchronous. Instead, we introduce a new gopls.vulncheck command to run synchronously. We also introduce a new "vulncheck" codelens setting to control the availability of codelenses for this new command. For expedience, the command handler is simply copied rather than refactored, and minimal tests are added/modified to test the new command. Hopefully we can migrate everything to the new command soon, and delete the old command. For golang/vscode-go#3572 Change-Id: Ib3cffd5fd038813680087fa1916127663f377581 Reviewed-on: https://go-review.googlesource.com/c/tools/+/627556 Reviewed-by: Alan Donovan <adonovan@google.com> Auto-Submit: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent c531f1b commit 87ac91f

File tree

9 files changed

+302
-71
lines changed

9 files changed

+302
-71
lines changed

gopls/doc/codelenses.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,15 @@ Default: off
9797

9898
File type: Go
9999

100-
## `run_govulncheck`: Run govulncheck
100+
## `run_govulncheck`: Run govulncheck (legacy)
101101

102102

103-
This codelens source annotates the `module` directive in a
104-
go.mod file with a command to run Govulncheck.
103+
This codelens source annotates the `module` directive in a go.mod file
104+
with a command to run Govulncheck asynchronously.
105105

106-
[Govulncheck](https://go.dev/blog/vuln) is a static
107-
analysis tool that computes the set of functions reachable
108-
within your application, including dependencies;
109-
queries a database of known security vulnerabilities; and
106+
[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that
107+
computes the set of functions reachable within your application, including
108+
dependencies; queries a database of known security vulnerabilities; and
110109
reports any potential problems it finds.
111110

112111

@@ -157,4 +156,20 @@ Default: on
157156

158157
File type: go.mod
159158

159+
## `vulncheck`: Run govulncheck
160+
161+
162+
This codelens source annotates the `module` directive in a go.mod file
163+
with a command to run govulncheck synchronously.
164+
165+
[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that
166+
computes the set of functions reachable within your application, including
167+
dependencies; queries a database of known security vulnerabilities; and
168+
reports any potential problems it finds.
169+
170+
171+
Default: off
172+
173+
File type: go.mod
174+
160175
<!-- END Lenses: DO NOT MANUALLY EDIT THIS SECTION -->

gopls/internal/doc/api.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@
811811
},
812812
{
813813
"Name": "\"run_govulncheck\"",
814-
"Doc": "`\"run_govulncheck\"`: Run govulncheck\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run Govulncheck.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static\nanalysis tool that computes the set of functions reachable\nwithin your application, including dependencies;\nqueries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
814+
"Doc": "`\"run_govulncheck\"`: Run govulncheck (legacy)\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
815815
"Default": "false"
816816
},
817817
{
@@ -833,6 +833,11 @@
833833
"Name": "\"vendor\"",
834834
"Doc": "`\"vendor\"`: Update vendor directory\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
835835
"Default": "true"
836+
},
837+
{
838+
"Name": "\"vulncheck\"",
839+
"Doc": "`\"vulncheck\"`: Run govulncheck\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
840+
"Default": "false"
836841
}
837842
]
838843
},
@@ -953,8 +958,8 @@
953958
{
954959
"FileType": "go.mod",
955960
"Lens": "run_govulncheck",
956-
"Title": "Run govulncheck",
957-
"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run Govulncheck.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static\nanalysis tool that computes the set of functions reachable\nwithin your application, including dependencies;\nqueries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
961+
"Title": "Run govulncheck (legacy)",
962+
"Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
958963
"Default": false
959964
},
960965
{
@@ -977,6 +982,13 @@
977982
"Title": "Update vendor directory",
978983
"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
979984
"Default": true
985+
},
986+
{
987+
"FileType": "go.mod",
988+
"Lens": "vulncheck",
989+
"Title": "Run govulncheck",
990+
"Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
991+
"Default": false
980992
}
981993
],
982994
"Analyzers": [

gopls/internal/mod/code_lens.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ import (
2121
// CodeLensSources returns the sources of code lenses for go.mod files.
2222
func CodeLensSources() map[settings.CodeLensSource]cache.CodeLensSourceFunc {
2323
return map[settings.CodeLensSource]cache.CodeLensSourceFunc{
24-
settings.CodeLensUpgradeDependency: upgradeLenses, // commands: CheckUpgrades, UpgradeDependency
25-
settings.CodeLensTidy: tidyLens, // commands: Tidy
26-
settings.CodeLensVendor: vendorLens, // commands: Vendor
27-
settings.CodeLensRunGovulncheck: vulncheckLenses, // commands: RunGovulncheck
24+
settings.CodeLensUpgradeDependency: upgradeLenses, // commands: CheckUpgrades, UpgradeDependency
25+
settings.CodeLensTidy: tidyLens, // commands: Tidy
26+
settings.CodeLensVendor: vendorLens, // commands: Vendor
27+
settings.CodeLensVulncheck: vulncheckLenses, // commands: Vulncheck
28+
settings.CodeLensRunGovulncheck: runGovulncheckLenses, // commands: RunGovulncheck
2829
}
2930
}
3031

@@ -162,6 +163,29 @@ func vulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand
162163
return nil, err
163164
}
164165

166+
vulncheck := command.NewVulncheckCommand("Run govulncheck", command.VulncheckArgs{
167+
URI: uri,
168+
Pattern: "./...",
169+
})
170+
return []protocol.CodeLens{
171+
{Range: rng, Command: vulncheck},
172+
}, nil
173+
}
174+
175+
func runGovulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
176+
pm, err := snapshot.ParseMod(ctx, fh)
177+
if err != nil || pm.File == nil {
178+
return nil, err
179+
}
180+
// Place the codelenses near the module statement.
181+
// A module may not have the require block,
182+
// but vulnerabilities can exist in standard libraries.
183+
uri := fh.URI()
184+
rng, err := moduleStmtRange(fh, pm)
185+
if err != nil {
186+
return nil, err
187+
}
188+
165189
vulncheck := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{
166190
URI: uri,
167191
Pattern: "./...",

gopls/internal/protocol/command/command_gen.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/protocol/command/interface.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,30 @@ type Interface interface {
186186
// runner.
187187
StopProfile(context.Context, StopProfileArgs) (StopProfileResult, error)
188188

189-
// RunGovulncheck: Run vulncheck
189+
// GoVulncheck: run vulncheck synchronously.
190190
//
191191
// Run vulnerability check (`govulncheck`).
192192
//
193-
// This command is asynchronous; clients must wait for the 'end' progress notification.
193+
// This command is synchronous, and returns the govulncheck result.
194+
Vulncheck(context.Context, VulncheckArgs) (VulncheckResult, error)
195+
196+
// RunGovulncheck: Run vulncheck asynchronously.
197+
//
198+
// Run vulnerability check (`govulncheck`).
199+
//
200+
// This command is asynchronous; clients must wait for the 'end' progress
201+
// notification and then retrieve results using gopls.fetch_vulncheck_result.
202+
//
203+
// Deprecated: clients should call gopls.vulncheck instead, which returns the
204+
// actual vulncheck result.
194205
RunGovulncheck(context.Context, VulncheckArgs) (RunVulncheckResult, error)
195206

196207
// FetchVulncheckResult: Get known vulncheck result
197208
//
198209
// Fetch the result of latest vulnerability check (`govulncheck`).
210+
//
211+
// Deprecated: clients should call gopls.vulncheck instead, which returns the
212+
// actual vulncheck result.
199213
FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error)
200214

201215
// MemStats: Fetch memory statistics
@@ -506,13 +520,12 @@ type VulncheckArgs struct {
506520
type RunVulncheckResult struct {
507521
// Token holds the progress token for LSP workDone reporting of the vulncheck
508522
// invocation.
509-
//
510-
// Deprecated: previously, this was used as a signal to retrieve the result
511-
// using gopls.fetch_vulncheck_result. Clients should ignore this field:
512-
// gopls.vulncheck now runs synchronously, and returns a result in the Result
513-
// field.
514523
Token protocol.ProgressToken
524+
}
515525

526+
// GovulncheckResult holds the result of synchronously running the vulncheck
527+
// command.
528+
type VulncheckResult struct {
516529
// Result holds the result of running vulncheck.
517530
Result *vulncheck.Result
518531
}

gopls/internal/server/command.go

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"errors"
1212
"fmt"
1313
"io"
14+
"log"
1415
"os"
1516
"path/filepath"
1617
"regexp"
@@ -392,6 +393,22 @@ func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run command
392393
return err
393394
}
394395

396+
// For legacy reasons, gopls.run_govulncheck must run asynchronously.
397+
// TODO(golang/vscode-go#3572): remove this (along with the
398+
// gopls.run_govulncheck command entirely) once VS Code only uses the new
399+
// gopls.vulncheck command.
400+
if c.params.Command == "gopls.run_govulncheck" {
401+
if cfg.progress == "" {
402+
log.Fatalf("asynchronous command gopls.run_govulncheck does not enable progress reporting")
403+
}
404+
go func() {
405+
if err := runcmd(); err != nil {
406+
showMessage(ctx, c.s.client, protocol.Error, err.Error())
407+
}
408+
}()
409+
return nil
410+
}
411+
395412
return runcmd()
396413
}
397414

@@ -1210,21 +1227,91 @@ func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.U
12101227

12111228
const GoVulncheckCommandTitle = "govulncheck"
12121229

1230+
func (c *commandHandler) Vulncheck(ctx context.Context, args command.VulncheckArgs) (command.VulncheckResult, error) {
1231+
if args.URI == "" {
1232+
return command.VulncheckResult{}, errors.New("VulncheckArgs is missing URI field")
1233+
}
1234+
1235+
var commandResult command.VulncheckResult
1236+
err := c.run(ctx, commandConfig{
1237+
progress: GoVulncheckCommandTitle,
1238+
requireSave: true, // govulncheck cannot honor overlays
1239+
forURI: args.URI,
1240+
}, func(ctx context.Context, deps commandDeps) error {
1241+
jsonrpc2.Async(ctx) // run this in parallel with other requests: vulncheck can be slow.
1242+
1243+
workDoneWriter := progress.NewWorkDoneWriter(ctx, deps.work)
1244+
dir := filepath.Dir(args.URI.Path())
1245+
pattern := args.Pattern
1246+
1247+
result, err := scan.RunGovulncheck(ctx, pattern, deps.snapshot, dir, workDoneWriter)
1248+
if err != nil {
1249+
return err
1250+
}
1251+
commandResult.Result = result
1252+
1253+
snapshot, release, err := c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
1254+
Vulns: map[protocol.DocumentURI]*vulncheck.Result{args.URI: result},
1255+
})
1256+
if err != nil {
1257+
return err
1258+
}
1259+
defer release()
1260+
1261+
// Diagnosing with the background context ensures new snapshots are fully
1262+
// diagnosed.
1263+
c.s.diagnoseSnapshot(snapshot.BackgroundContext(), snapshot, nil, 0)
1264+
1265+
affecting := make(map[string]bool, len(result.Entries))
1266+
for _, finding := range result.Findings {
1267+
if len(finding.Trace) > 1 { // at least 2 frames if callstack exists (vulnerability, entry)
1268+
affecting[finding.OSV] = true
1269+
}
1270+
}
1271+
if len(affecting) == 0 {
1272+
showMessage(ctx, c.s.client, protocol.Info, "No vulnerabilities found")
1273+
return nil
1274+
}
1275+
affectingOSVs := make([]string, 0, len(affecting))
1276+
for id := range affecting {
1277+
affectingOSVs = append(affectingOSVs, id)
1278+
}
1279+
sort.Strings(affectingOSVs)
1280+
1281+
showMessage(ctx, c.s.client, protocol.Warning, fmt.Sprintf("Found %v", strings.Join(affectingOSVs, ", ")))
1282+
1283+
return nil
1284+
})
1285+
if err != nil {
1286+
return command.VulncheckResult{}, err
1287+
}
1288+
return commandResult, nil
1289+
}
1290+
1291+
// RunGovulncheck is like Vulncheck (in fact, a copy), but is tweaked slightly
1292+
// to run asynchronously rather than return a result.
1293+
//
1294+
// This logic was copied, rather than factored out, as this implementation is
1295+
// slated for deletion.
1296+
//
1297+
// TODO(golang/vscode-go#3572)
12131298
func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) {
12141299
if args.URI == "" {
12151300
return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field")
12161301
}
12171302

1218-
var commandResult command.RunVulncheckResult
1303+
// Return the workdone token so that clients can identify when this
1304+
// vulncheck invocation is complete.
1305+
//
1306+
// Since the run function executes asynchronously, we use a channel to
1307+
// synchronize the start of the run and return the token.
1308+
tokenChan := make(chan protocol.ProgressToken, 1)
12191309
err := c.run(ctx, commandConfig{
12201310
progress: GoVulncheckCommandTitle,
12211311
requireSave: true, // govulncheck cannot honor overlays
12221312
forURI: args.URI,
12231313
}, func(ctx context.Context, deps commandDeps) error {
1224-
// For compatibility with the legacy asynchronous API, return the workdone
1225-
// token that clients used to use to identify when this vulncheck
1226-
// invocation is complete.
1227-
commandResult.Token = deps.work.Token()
1314+
tokenChan <- deps.work.Token()
12281315

12291316
jsonrpc2.Async(ctx) // run this in parallel with other requests: vulncheck can be slow.
12301317

@@ -1236,7 +1323,6 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch
12361323
if err != nil {
12371324
return err
12381325
}
1239-
commandResult.Result = result
12401326

12411327
snapshot, release, err := c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
12421328
Vulns: map[protocol.DocumentURI]*vulncheck.Result{args.URI: result},
@@ -1273,7 +1359,12 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch
12731359
if err != nil {
12741360
return command.RunVulncheckResult{}, err
12751361
}
1276-
return commandResult, nil
1362+
select {
1363+
case <-ctx.Done():
1364+
return command.RunVulncheckResult{}, ctx.Err()
1365+
case token := <-tokenChan:
1366+
return command.RunVulncheckResult{Token: token}, nil
1367+
}
12771368
}
12781369

12791370
// MemStats implements the MemStats command. It returns an error as a

0 commit comments

Comments
 (0)