internal/scan: improve error message for govulncheck

- Previously, an error was not returned if the govulncheck function was
  called without any patterns.
- Now if patterns are not included, then an explicit error is returned
  indicating to users that they should specify patterns or simply use
  `govulncheck ./...`.

Fixes golang/go#77167

Change-Id: I539246a6f18b50937068ce9c984d0d3e4973c534
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/736180
Reviewed-by: Roland Shoemaker <roland@golang.org>
Auto-Submit: Ethan Lee <ethanalee@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/scan/errors.go b/internal/scan/errors.go
index c28e3c5..905e1e6 100644
--- a/internal/scan/errors.go
+++ b/internal/scan/errors.go
@@ -11,7 +11,7 @@
 
 //lint:file-ignore ST1005 Ignore staticcheck message about error formatting
 var (
-	// ErrVulnerabilitiesFound indicates that vulnerabilities were detected
+	// errVulnerabilitiesFound indicates that vulnerabilities were detected
 	// when running govulncheck. This returns exit status 3 when running
 	// without the -json flag.
 	errVulnerabilitiesFound = &exitCodeError{message: "vulnerabilities found", code: 3}
@@ -25,6 +25,18 @@
 	// govulncheck and exit with status 2.
 	errUsage = &exitCodeError{message: "invalid usage", code: 2}
 
+	// errNoPatterns indicates that no package patterns were provided if the scan level is WantPackages.
+	errNoPatterns = &exitCodeError{
+		message: "no package patterns provided\n\nTo scan the current module, run: govulncheck ./...",
+		code:    2,
+	}
+
+	// errNoPackagesMatched indicates that the provided patterns matched no packages if scan level is WantPackages.
+	errNoPackagesMatched = &exitCodeError{
+		message: "no packages matched the provided patterns",
+		code:    2,
+	}
+
 	// errGoVersionMismatch is used to indicate that there is a mismatch between
 	// the Go version used to build govulncheck and the one currently on PATH.
 	errGoVersionMismatch = errors.New(`Loading packages failed, possibly due to a mismatch between the Go version
diff --git a/internal/scan/run_test.go b/internal/scan/run_test.go
index 736804c..deb83a0 100644
--- a/internal/scan/run_test.go
+++ b/internal/scan/run_test.go
@@ -5,7 +5,10 @@
 package scan
 
 import (
+	"bytes"
+	"context"
 	"runtime/debug"
+	"strings"
 	"testing"
 )
 
@@ -24,3 +27,32 @@
 		t.Errorf("got %s; want %s", got.ScannerVersion, want)
 	}
 }
+
+func TestRunGovulncheck_NoPatternsError(t *testing.T) {
+	ctx := context.Background()
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+
+	err := RunGovulncheck(ctx, nil, nil, stdout, stderr, []string{})
+	if err == nil {
+		t.Fatal("expected RunGovulncheck to return an error for missing patterns, got nil")
+	}
+
+	wantMsg := "no package patterns provided"
+	if !strings.Contains(err.Error(), wantMsg) {
+		t.Errorf("got error: %v; want error containing %q", err, wantMsg)
+	}
+}
+
+func TestRunGovulncheck_ModuleModeNoPatterns(t *testing.T) {
+	ctx := context.Background()
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+
+	// ScanLevelModule does not require packages, so it should not error out on empty patterns.
+	err := RunGovulncheck(ctx, nil, nil, stdout, stderr, []string{"-scan", "module"})
+
+	if err != nil && strings.Contains(err.Error(), "no package patterns provided") {
+		t.Errorf("unexpected 'no package patterns' error in module mode: %v", err)
+	}
+}
diff --git a/internal/scan/source.go b/internal/scan/source.go
index e923211..b34e443 100644
--- a/internal/scan/source.go
+++ b/internal/scan/source.go
@@ -24,7 +24,7 @@
 	defer derrors.Wrap(&err, "govulncheck")
 
 	if cfg.ScanLevel.WantPackages() && len(cfg.patterns) == 0 {
-		return nil // don't throw an error here
+		return errNoPatterns
 	}
 	if !gomodExists(dir) {
 		return errNoGoMod
@@ -43,7 +43,7 @@
 	}
 
 	if cfg.ScanLevel.WantPackages() && len(graph.TopPkgs()) == 0 {
-		return nil // early exit
+		return errNoPackagesMatched
 	}
 	return vulncheck.Source(ctx, handler, &cfg.Config, client, graph)
 }