blob: 8badc879e1b9cc4c4b4d215da2f19efd9f213e3c [file] [log] [blame]
// 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.
//go:build go1.18
// +build go1.18
package misc
import (
"context"
"encoding/json"
"path/filepath"
"sort"
"strings"
"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"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/tests/compare"
"golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/gopls/internal/vulncheck/vulntest"
"golang.org/x/tools/internal/testenv"
)
func TestRunGovulncheckError(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- foo.go --
package foo
`
Run(t, files, func(t *testing.T, env *Env) {
cmd, err := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{
URI: "/invalid/file/url", // invalid arg
})
if err != nil {
t.Fatal(err)
}
params := &protocol.ExecuteCommandParams{
Command: command.RunGovulncheck.ID(),
Arguments: cmd.Arguments,
}
response, err := env.Editor.ExecuteCommand(env.Ctx, params)
// We want an error!
if err == nil {
t.Errorf("got success, want invalid file URL error: %v", response)
}
})
}
func TestRunGovulncheckError2(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- foo.go --
package foo
func F() { // build error incomplete
`
WithOptions(
EnvVars{
"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
},
Settings{
"codelenses": map[string]bool{
"run_govulncheck": true,
},
},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
var result command.RunVulncheckResult
env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
var ws WorkStatus
env.Await(
CompletedProgress(result.Token, &ws),
)
wantEndMsg, wantMsgPart := "failed", "failed to load packages due to errors"
if ws.EndMsg != "failed" || !strings.Contains(ws.Msg, wantMsgPart) {
t.Errorf("work status = %+v, want {EndMessage: %q, Message: %q}", ws, wantEndMsg, wantMsgPart)
}
})
}
const vulnsData = `
-- GO-2022-01.yaml --
modules:
- module: golang.org/amod
versions:
- introduced: 1.0.0
- fixed: 1.0.4
- introduced: 1.1.2
packages:
- package: golang.org/amod/avuln
symbols:
- VulnData.Vuln1
- VulnData.Vuln2
description: >
vuln in amod
references:
- href: pkg.go.dev/vuln/GO-2022-01
-- GO-2022-03.yaml --
modules:
- module: golang.org/amod
versions:
- introduced: 1.0.0
- fixed: 1.0.6
packages:
- package: golang.org/amod/avuln
symbols:
- nonExisting
description: >
unaffecting vulnerability
-- GO-2022-02.yaml --
modules:
- module: golang.org/bmod
packages:
- package: golang.org/bmod/bvuln
symbols:
- Vuln
description: |
vuln in bmod
This is a long description
of this vulnerability.
references:
- href: pkg.go.dev/vuln/GO-2022-03
-- GO-2022-04.yaml --
modules:
- module: golang.org/bmod
packages:
- package: golang.org/bmod/unused
symbols:
- Vuln
description: |
vuln in bmod/somtrhingelse
references:
- href: pkg.go.dev/vuln/GO-2022-04
-- GOSTDLIB.yaml --
modules:
- module: stdlib
versions:
- introduced: 1.18.0
packages:
- package: archive/zip
symbols:
- OpenReader
references:
- href: pkg.go.dev/vuln/GOSTDLIB
`
func TestRunGovulncheckStd(t *testing.T) {
testenv.NeedsGo1Point(t, 18)
const files = `
-- go.mod --
module mod.com
go 1.18
-- main.go --
package main
import (
"archive/zip"
"fmt"
)
func main() {
_, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB
fmt.Println(err)
}
`
db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
if err != nil {
t.Fatal(err)
}
defer db.Clean()
WithOptions(
EnvVars{
// Let the analyzer read vulnerabilities data from the testdata/vulndb.
"GOVULNDB": db.URI(),
// When fetchinging stdlib package vulnerability info,
// behave as if our go version is go1.18 for this testing.
// The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
vulncheck.GoVersionForVulnTest: "go1.18",
"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
},
Settings{
"codelenses": map[string]bool{
"run_govulncheck": true,
},
},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
// Test CodeLens is present.
lenses := env.CodeLens("go.mod")
const wantCommand = "gopls." + string(command.RunGovulncheck)
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.
var result command.RunVulncheckResult
env.ExecuteCommand(&protocol.ExecuteCommandParams{
Command: lens.Command.Command,
Arguments: lens.Command.Arguments,
}, &result)
env.OnceMet(
CompletedProgress(result.Token, nil),
ShownMessage("Found GOSTDLIB"),
NoDiagnostics(ForFile("go.mod")),
)
testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
"go.mod": {IDs: []string{"GOSTDLIB"}, Mode: govulncheck.ModeGovulncheck}})
})
}
func TestFetchVulncheckResultStd(t *testing.T) {
testenv.NeedsGo1Point(t, 18)
const files = `
-- go.mod --
module mod.com
go 1.18
-- main.go --
package main
import (
"archive/zip"
"fmt"
)
func main() {
_, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB
fmt.Println(err)
}
`
db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
if err != nil {
t.Fatal(err)
}
defer db.Clean()
WithOptions(
EnvVars{
// Let the analyzer read vulnerabilities data from the testdata/vulndb.
"GOVULNDB": db.URI(),
// When fetchinging stdlib package vulnerability info,
// behave as if our go version is go1.18 for this testing.
vulncheck.GoVersionForVulnTest: "go1.18",
"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
},
Settings{"ui.diagnostic.vulncheck": "Imports"},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
env.AfterChange(
NoDiagnostics(ForFile("go.mod")),
// we don't publish diagnostics for standard library vulnerability yet.
)
testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
"go.mod": {
IDs: []string{"GOSTDLIB"},
Mode: govulncheck.ModeImports,
},
})
})
}
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
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.IDs)
}
got := map[string]fetchVulncheckResult{}
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] = 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)
}
}
const workspace1 = `
-- go.mod --
module golang.org/entry
go 1.18
require golang.org/cmod v1.1.3
require (
golang.org/amod v1.0.0 // indirect
golang.org/bmod v0.5.0 // indirect
)
-- go.sum --
golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k=
golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E=
golang.org/bmod v0.5.0 h1:KgvUulMyMiYRB7suKA0x+DfWRVdeyPgVJvcishTH+ng=
golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c=
golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ=
golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA=
-- x/x.go --
package x
import (
"golang.org/cmod/c"
"golang.org/entry/y"
)
func X() {
c.C1().Vuln1() // vuln use: X -> Vuln1
}
func CallY() {
y.Y() // vuln use: CallY -> y.Y -> bvuln.Vuln
}
-- y/y.go --
package y
import "golang.org/cmod/c"
func Y() {
c.C2()() // vuln use: Y -> bvuln.Vuln
}
`
// cmod/c imports amod/avuln and bmod/bvuln.
const proxy1 = `
-- golang.org/cmod@v1.1.3/go.mod --
module golang.org/cmod
go 1.12
-- golang.org/cmod@v1.1.3/c/c.go --
package c
import (
"golang.org/amod/avuln"
"golang.org/bmod/bvuln"
)
type I interface {
Vuln1()
}
func C1() I {
v := avuln.VulnData{}
v.Vuln2() // vuln use
return v
}
func C2() func() {
return bvuln.Vuln
}
-- golang.org/amod@v1.0.0/go.mod --
module golang.org/amod
go 1.14
-- golang.org/amod@v1.0.0/avuln/avuln.go --
package avuln
type VulnData struct {}
func (v VulnData) Vuln1() {}
func (v VulnData) Vuln2() {}
-- golang.org/amod@v1.0.4/go.mod --
module golang.org/amod
go 1.14
-- golang.org/amod@v1.0.4/avuln/avuln.go --
package avuln
type VulnData struct {}
func (v VulnData) Vuln1() {}
func (v VulnData) Vuln2() {}
-- golang.org/bmod@v0.5.0/go.mod --
module golang.org/bmod
go 1.14
-- golang.org/bmod@v0.5.0/bvuln/bvuln.go --
package bvuln
func Vuln() {
// something evil
}
-- golang.org/bmod@v0.5.0/unused/unused.go --
package unused
func Vuln() {
// something evil
}
-- golang.org/amod@v1.0.6/go.mod --
module golang.org/amod
go 1.14
-- golang.org/amod@v1.0.6/avuln/avuln.go --
package avuln
type VulnData struct {}
func (v VulnData) Vuln1() {}
func (v VulnData) Vuln2() {}
`
func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) {
db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
if err != nil {
return nil, nil, nil
}
settings := Settings{
"codelenses": map[string]bool{
"run_govulncheck": true,
},
}
ev := EnvVars{
// Let the analyzer read vulnerabilities data from the testdata/vulndb.
"GOVULNDB": db.URI(),
// When fetching stdlib package vulnerability info,
// behave as if our go version is go1.18 for this testing.
// The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
vulncheck.GoVersionForVulnTest: "go1.18",
"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
"GOSUMDB": "off",
}
return db, []RunOption{ProxyFiles(proxyData), ev, settings}, nil
}
func TestRunVulncheckPackageDiagnostics(t *testing.T) {
testenv.NeedsGo1Point(t, 18)
db, opts0, err := vulnTestEnv(vulnsData, proxy1)
if err != nil {
t.Fatal(err)
}
defer db.Clean()
checkVulncheckDiagnostics := func(env *Env, t *testing.T) {
env.OpenFile("go.mod")
gotDiagnostics := &protocol.PublishDiagnosticsParams{}
env.AfterChange(
Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
ReadDiagnostics("go.mod", gotDiagnostics),
)
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": {
diagnostics: []vulnDiag{
{
msg: "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.",
severity: protocol.SeverityInformation,
source: string(source.Vulncheck),
codeActions: []string{
"Run govulncheck to verify",
"Upgrade to v1.0.6",
"Upgrade to latest",
},
},
},
codeActions: []string{
"Run govulncheck to verify",
"Upgrade to v1.0.6",
"Upgrade to latest",
},
hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"},
},
"golang.org/bmod": {
diagnostics: []vulnDiag{
{
msg: "golang.org/bmod has a vulnerability GO-2022-02.",
severity: protocol.SeverityInformation,
source: string(source.Vulncheck),
codeActions: []string{
"Run govulncheck to verify",
},
},
},
codeActions: []string{
"Run govulncheck to verify",
},
hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
},
}
for pattern, want := range wantVulncheckDiagnostics {
modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics)
gotActions := env.CodeAction("go.mod", modPathDiagnostics)
if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff)
continue
}
}
}
wantNoVulncheckDiagnostics := func(env *Env, t *testing.T) {
env.OpenFile("go.mod")
gotDiagnostics := &protocol.PublishDiagnosticsParams{}
env.AfterChange(
ReadDiagnostics("go.mod", gotDiagnostics),
)
if len(gotDiagnostics.Diagnostics) > 0 {
t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics))
}
testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{})
}
for _, tc := range []struct {
name string
setting Settings
wantDiagnostics bool
}{
{"imports", Settings{"ui.diagnostic.vulncheck": "Imports"}, true},
{"default", Settings{}, false},
{"invalid", Settings{"ui.diagnostic.vulncheck": "invalid"}, false},
} {
t.Run(tc.name, func(t *testing.T) {
// override the settings options to enable diagnostics
opts := append(opts0, tc.setting)
WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) {
// TODO(hyangah): implement it, so we see GO-2022-01, GO-2022-02, and GO-2022-03.
// Check that the actions we get when including all diagnostics at a location return the same result
if tc.wantDiagnostics {
checkVulncheckDiagnostics(env, t)
} else {
wantNoVulncheckDiagnostics(env, t)
}
if tc.name == "imports" && tc.wantDiagnostics {
// test we get only govulncheck-based diagnostics after "run govulncheck".
var result command.RunVulncheckResult
env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
gotDiagnostics := &protocol.PublishDiagnosticsParams{}
env.OnceMet(
CompletedProgress(result.Token, nil),
ShownMessage("Found"),
)
env.OnceMet(
Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
ReadDiagnostics("go.mod", gotDiagnostics),
)
// We expect only one diagnostic for GO-2022-02.
count := 0
for _, diag := range gotDiagnostics.Diagnostics {
if strings.Contains(diag.Message, "GO-2022-02") {
count++
if got, want := diag.Severity, protocol.SeverityWarning; got != want {
t.Errorf("Diagnostic for GO-2022-02 = %v, want %v", got, want)
}
}
}
if count != 1 {
t.Errorf("Unexpected number of diagnostics about GO-2022-02 = %v, want 1:\n%+v", count, stringify(gotDiagnostics))
}
}
})
})
}
}
func stringify(a interface{}) string {
data, _ := json.Marshal(a)
return string(data)
}
func TestRunVulncheckWarning(t *testing.T) {
testenv.NeedsGo1Point(t, 18)
db, opts, err := vulnTestEnv(vulnsData, proxy1)
if err != nil {
t.Fatal(err)
}
defer db.Clean()
WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
var result command.RunVulncheckResult
env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
gotDiagnostics := &protocol.PublishDiagnosticsParams{}
env.OnceMet(
CompletedProgress(result.Token, nil),
ShownMessage("Found"),
)
// Vulncheck diagnostics asynchronous to the vulncheck command.
env.OnceMet(
Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
ReadDiagnostics("go.mod", gotDiagnostics),
)
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\(\)`).Range.Start
env.OpenFile("y/y.go")
lineY := env.RegexpSearch("y/y.go", `c\.C2\(\)\(\)`).Range.Start
wantDiagnostics := map[string]vulnDiagExpectation{
"golang.org/amod": {
applyAction: "Upgrade to v1.0.6",
diagnostics: []vulnDiag{
{
msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.",
severity: protocol.SeverityWarning,
source: string(source.Govulncheck),
codeActions: []string{
"Upgrade to v1.0.4",
"Upgrade to latest",
"Reset govulncheck result",
},
relatedInfo: []vulnRelatedInfo{
{"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1
{"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2
},
},
{
msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.",
severity: protocol.SeverityInformation,
source: string(source.Govulncheck),
codeActions: []string{
"Upgrade to v1.0.6",
"Upgrade to latest",
"Reset govulncheck result",
},
relatedInfo: []vulnRelatedInfo{
{"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1
{"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2
},
},
},
codeActions: []string{
"Upgrade to v1.0.6",
"Upgrade to latest",
"Reset govulncheck result",
},
hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"},
},
"golang.org/bmod": {
diagnostics: []vulnDiag{
{
msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.",
severity: protocol.SeverityWarning,
source: string(source.Govulncheck),
codeActions: []string{
"Reset govulncheck result", // no fix, but we should give an option to reset.
},
relatedInfo: []vulnRelatedInfo{
{"y.go", uint32(lineY.Line), "[GO-2022-02]"}, // bvuln.Vuln
},
},
},
codeActions: []string{
"Reset govulncheck result", // no fix, but we should give an option to reset.
},
hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
},
}
for mod, want := range wantDiagnostics {
modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics)
// Check that the actions we get when including all diagnostics at a location return the same result
gotActions := env.CodeAction("go.mod", modPathDiagnostics)
if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff)
continue
}
// Apply the code action matching applyAction.
if want.applyAction == "" {
continue
}
for _, action := range gotActions {
if action.Title == want.applyAction {
env.ApplyCodeAction(action)
break
}
}
}
env.Await(env.DoneWithChangeWatchedFiles())
wantGoMod := `module golang.org/entry
go 1.18
require golang.org/cmod v1.1.3
require (
golang.org/amod v1.0.6 // indirect
golang.org/bmod v0.5.0 // indirect
)
`
if got := env.BufferText("go.mod"); got != wantGoMod {
t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got))
}
})
}
func diffCodeActions(gotActions []protocol.CodeAction, want []string) string {
var gotTitles []string
for _, ca := range gotActions {
gotTitles = append(gotTitles, ca.Title)
}
return cmp.Diff(want, gotTitles)
}
const workspace2 = `
-- go.mod --
module golang.org/entry
go 1.18
require golang.org/bmod v0.5.0
-- go.sum --
golang.org/bmod v0.5.0 h1:MT/ysNRGbCiURc5qThRFWaZ5+rK3pQRPo9w7dYZfMDk=
golang.org/bmod v0.5.0/go.mod h1:k+zl+Ucu4yLIjndMIuWzD/MnOHy06wqr3rD++y0abVs=
-- x/x.go --
package x
import "golang.org/bmod/bvuln"
func F() {
// Calls a benign func in bvuln.
bvuln.OK()
}
`
const proxy2 = `
-- golang.org/bmod@v0.5.0/bvuln/bvuln.go --
package bvuln
func Vuln() {} // vulnerable.
func OK() {} // ok.
`
func TestGovulncheckInfo(t *testing.T) {
testenv.NeedsGo1Point(t, 18)
db, opts, err := vulnTestEnv(vulnsData, proxy2)
if err != nil {
t.Fatal(err)
}
defer db.Clean()
WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
var result command.RunVulncheckResult
env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
gotDiagnostics := &protocol.PublishDiagnosticsParams{}
env.OnceMet(
CompletedProgress(result.Token, nil),
ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities.
)
// Vulncheck diagnostics asynchronous to the vulncheck command.
env.OnceMet(
Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
ReadDiagnostics("go.mod", gotDiagnostics),
)
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.
wantDiagnostics := map[string]vulnDiagExpectation{
"golang.org/bmod": {
diagnostics: []vulnDiag{
{
msg: "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.",
severity: protocol.SeverityInformation,
source: string(source.Govulncheck),
codeActions: []string{
"Reset govulncheck result",
},
},
},
codeActions: []string{
"Reset govulncheck result",
},
hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."},
},
}
var allActions []protocol.CodeAction
for mod, want := range wantDiagnostics {
modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics)
// Check that the actions we get when including all diagnostics at a location return the same result
gotActions := env.CodeAction("go.mod", modPathDiagnostics)
allActions = append(allActions, gotActions...)
if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff)
continue
}
}
// Clear Diagnostics by using one of the reset code actions.
var reset protocol.CodeAction
for _, a := range allActions {
if a.Title == "Reset govulncheck result" {
reset = a
break
}
}
if reset.Title != "Reset govulncheck result" {
t.Errorf("failed to find a 'Reset govulncheck result' code action, got %v", allActions)
}
env.ApplyCodeAction(reset)
env.Await(NoDiagnostics(ForFile("go.mod")))
})
}
// testVulnDiagnostics finds the require or module statement line for the requireMod in go.mod file
// and runs checks if diagnostics and code actions associated with the line match expectation.
func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic {
t.Helper()
loc := env.RegexpSearch("go.mod", pattern)
var modPathDiagnostics []protocol.Diagnostic
for _, w := range want.diagnostics {
// Find the diagnostics at loc.start.
var diag *protocol.Diagnostic
for _, g := range got.Diagnostics {
g := g
if g.Range.Start == loc.Range.Start && w.msg == g.Message {
modPathDiagnostics = append(modPathDiagnostics, g)
diag = &g
break
}
}
if diag == nil {
t.Errorf("no diagnostic at %q matching %q found\n", pattern, w.msg)
continue
}
if diag.Severity != w.severity || diag.Source != w.source {
t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source)
}
sort.Slice(w.relatedInfo, func(i, j int) bool { return w.relatedInfo[i].less(w.relatedInfo[j]) })
if got, want := summarizeRelatedInfo(diag.RelatedInformation), w.relatedInfo; !cmp.Equal(got, want) {
t.Errorf("related info for %q do not match, want %v, got %v\n", w.msg, want, got)
}
// Check expected code actions appear.
gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag})
if diff := diffCodeActions(gotActions, w.codeActions); diff != "" {
t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff)
continue
}
}
// Check that useful info is supplemented as hover.
if len(want.hover) > 0 {
hover, _ := env.Hover(loc)
for _, part := range want.hover {
if !strings.Contains(hover.Value, part) {
t.Errorf("hover contents for %q do not match, want %v, got %v\n", pattern, strings.Join(want.hover, ","), hover.Value)
break
}
}
}
return modPathDiagnostics
}
// summarizeRelatedInfo converts protocol.DiagnosticRelatedInformation to vulnRelatedInfo
// that captures only the part that we want to test.
func summarizeRelatedInfo(rinfo []protocol.DiagnosticRelatedInformation) []vulnRelatedInfo {
var res []vulnRelatedInfo
for _, r := range rinfo {
filename := filepath.Base(r.Location.URI.SpanURI().Filename())
message, _, _ := strings.Cut(r.Message, " ")
line := r.Location.Range.Start.Line
res = append(res, vulnRelatedInfo{filename, line, message})
}
sort.Slice(res, func(i, j int) bool {
return res[i].less(res[j])
})
return res
}
type vulnRelatedInfo struct {
Filename string
Line uint32
Message string
}
type vulnDiag struct {
msg string
severity protocol.DiagnosticSeverity
// codeActions is a list titles of code actions that we get with this
// diagnostics as the context.
codeActions []string
// relatedInfo is related info message prefixed by the file base.
// See summarizeRelatedInfo.
relatedInfo []vulnRelatedInfo
// diagnostic source.
source string
}
func (i vulnRelatedInfo) less(j vulnRelatedInfo) bool {
if i.Filename != j.Filename {
return i.Filename < j.Filename
}
if i.Line != j.Line {
return i.Line < j.Line
}
return i.Message < j.Message
}
// 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 {
// applyAction is the title of the code action to run for this module.
// If empty, no code actions will be executed.
applyAction string
// diagnostics is the list of diagnostics we expect at the require line for
// the module path.
diagnostics []vulnDiag
// codeActions is a list titles of code actions that we get with context
// diagnostics.
codeActions []string
// hover message is the list of expected hover message parts for this go.mod require line.
// all parts must appear in the hover message.
hover []string
}