cmd/govulncheck: refactor the main functionality

Change-Id: Icfdc0ef40292724a720e94f6b7c3ea64a7b7a7ae
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/432179
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index c4b6321..dcb02e7 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -56,6 +56,71 @@
 		os.Exit(1)
 	}
 
+	patterns := flag.Args()
+
+	mode := "source"
+	if len(patterns) == 1 && isFile(patterns[0]) {
+		mode = "binary"
+	}
+	validateFlags(mode)
+
+	outputType := "text"
+	if *jsonFlag {
+		outputType = "json"
+	}
+	if outputType == "text" && *verboseFlag {
+		outputType = "verbose"
+	}
+
+	var buildFlags []string
+	if tagsFlag != nil {
+		buildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))}
+	}
+
+	Main(Config{
+		Analysis:     mode,
+		OutputFormat: outputType,
+		Patterns:     patterns,
+		SourceLoadConfig: packages.Config{
+			Dir:        filepath.FromSlash(dirFlag),
+			Tests:      *testFlag,
+			BuildFlags: buildFlags,
+		},
+	})
+}
+
+func validateFlags(mode string) {
+	switch mode {
+	case "binary":
+		if *testFlag {
+			die("govulncheck: the -test flag is invalid for binaries")
+		}
+		if tagsFlag != nil {
+			die("govulncheck: the -tags flag is invalid for binaries")
+		}
+	}
+}
+
+// Config is the configuration for Main.
+type Config struct {
+	// Analysis specifies the vulncheck analysis type. Valid types are "source" and "binary"
+	Analysis string
+	// OutputFormat specifies the result type. Valid types are:
+	//  "text": print human readable compact text output to STDOUT.
+	//  "verbose": print human readable verbose text output to STDOUT.
+	//  "json": print JSON-encoded vulncheck.Result.
+	OutputFormat string
+
+	// Patterns are either the binary path for "binary" analysis mode, or
+	// go package patterns for "source" analysis mode.
+	Patterns []string
+
+	// SourceLoadConfig specifies the package loading configuration.
+	SourceLoadConfig packages.Config
+}
+
+// Main is the main function for the govulncheck command line tool.
+func Main(cfg Config) {
 	dbs := []string{"https://vuln.go.dev"}
 	if GOVULNDB := os.Getenv("GOVULNDB"); GOVULNDB != "" {
 		dbs = strings.Split(GOVULNDB, ",")
@@ -68,8 +133,9 @@
 	}
 	vcfg := &vulncheck.Config{Client: dbClient, SourceGoVersion: goVersion()}
 
-	patterns := flag.Args()
-	if !*jsonFlag {
+	patterns := cfg.Patterns
+	format := cfg.OutputFormat
+	if format == "text" || format == "verbose" {
 		fmt.Printf(`govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
@@ -81,13 +147,8 @@
 		unaffected []*vulncheck.Vuln
 		ctx        = context.Background()
 	)
-	if len(patterns) == 1 && isFile(patterns[0]) {
-		if *testFlag {
-			die("govulncheck: the -test flag is invalid for binaries")
-		}
-		if tagsFlag != nil {
-			die("govulncheck: the -tags flag is invalid for binaries")
-		}
+	switch cfg.Analysis {
+	case "binary":
 		f, err := os.Open(patterns[0])
 		if err != nil {
 			die("govulncheck: %v", err)
@@ -97,18 +158,14 @@
 		if err != nil {
 			die("govulncheck: %v", err)
 		}
-	} else {
-		cfg := &packages.Config{
-			Dir:        filepath.FromSlash(dirFlag),
-			Tests:      *testFlag,
-			BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))},
-		}
+	case "source":
+		cfg := &cfg.SourceLoadConfig
 		pkgs, err = govulncheck.LoadPackages(cfg, patterns...)
 		if err != nil {
 			// Try to provide a meaningful and actionable error message.
-			if !fileExists(filepath.Join(dirFlag, "go.mod")) {
+			if !fileExists(filepath.Join(cfg.Dir, "go.mod")) {
 				die(noGoModErrorMessage)
-			} else if !fileExists(filepath.Join(dirFlag, "go.sum")) {
+			} else if !fileExists(filepath.Join(cfg.Dir, "go.sum")) {
 				die(noGoSumErrorMessage)
 			}
 			die("govulncheck: %v", err)
@@ -123,19 +180,24 @@
 		}
 		unaffected = filterUnaffected(r)
 		r.Vulns = filterCalled(r)
+	default:
+		die("govulncheck: invalid analysis mode %q", cfg.Analysis)
 	}
 
-	if *jsonFlag {
+	switch format {
+	case "json":
 		// Following golang.org/x/tools/go/analysis/singlechecker,
 		// return 0 exit code in -json mode.
 		writeJSON(r)
 		os.Exit(0)
+	case "text", "verbose":
+		// set of top-level packages, used to find representative symbols
+		ci := govulncheck.GetCallInfo(r, pkgs)
+		writeText(r, ci, unaffected, format == "verbose")
+	default:
+		die("govulncheck: unrecognized output type %q", cfg.OutputFormat)
 	}
 
-	// set of top-level packages, used to find representative symbols
-	ci := govulncheck.GetCallInfo(r, pkgs)
-	writeText(r, ci, unaffected)
-
 	// Following golang.org/x/tools/go/analysis/singlechecker,
 	// fail with 3 if there are findings (in this case, vulns).
 	exitCode := 0
@@ -214,7 +276,7 @@
 	lineLength = 55
 )
 
-func writeText(r *vulncheck.Result, ci *govulncheck.CallInfo, unaffected []*vulncheck.Vuln) {
+func writeText(r *vulncheck.Result, ci *govulncheck.CallInfo, unaffected []*vulncheck.Vuln, verbose bool) {
 	uniqueVulns := map[string]bool{}
 	for _, v := range r.Vulns {
 		uniqueVulns[v.OSV.ID] = true
@@ -238,7 +300,7 @@
 		fixed := fixedVersion(v0.PkgPath, v0.OSV.Affected)
 
 		var stacks string
-		if !*verboseFlag {
+		if !verbose {
 			stacks = defaultCallStacks(vg, ci)
 		} else {
 			stacks = verboseCallStacks(vg, ci)