cmd/govulncheck: inline LegacyRun into govulncheck main

This is the second in the series of CLs that attempts to refactor
govulncheck. The changes in this CL are:

- inline the logic of LegacyRun into cmd/govulncheck/main.go
- delete LegacyConfig and related data structures
- move all the accompanying logic to cmd/govulncheck
- expose db caching logic

This effectively results in the following structure.
internal/govulncheck contains the core logic for Source and Binary
analysis (including db caching). cmd/govulncheck contains the logic for
parsing the flags and printing the results.

The follow-up refactoring CLs will mainly focus on renaming of things
and addressing TODOs.

For golang/go#56042

Change-Id: Ib1fc26951420ae6d04e48205765a1019d117946f
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/447855
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/govulncheck/errors.go b/cmd/govulncheck/errors.go
similarity index 85%
rename from internal/govulncheck/errors.go
rename to cmd/govulncheck/errors.go
index 100ac75..04fe666 100644
--- a/internal/govulncheck/errors.go
+++ b/cmd/govulncheck/errors.go
@@ -2,27 +2,19 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package govulncheck
+package main
 
 import (
 	"errors"
+	"fmt"
 	"os"
 	"strings"
 
+	"golang.org/x/tools/go/packages"
 	"golang.org/x/vuln/vulncheck"
 )
 
 var (
-	// ErrContainsVulnerabilties is used to indicate that vulerabilities were
-	// found in the output of Run.
-	ErrContainsVulnerabilties = errors.New("module contains vulnerabilities")
-
-	// ErrInvalidAnalysisType indicates that an unsupported AnalysisType was passed to Config.
-	ErrInvalidAnalysisType = errors.New("invalid analysis type")
-
-	// ErrInvalidOutputType indicates that an unsupported OutputType was passed to Config.
-	ErrInvalidOutputType = errors.New("invalid output type")
-
 	// ErrErrGoVersionMismatch 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
@@ -53,6 +45,20 @@
 See https://go.dev/doc/modules/managing-dependencies for more information.`)
 )
 
+// A PackageError contains errors from loading a set of packages.
+type PackageError struct {
+	Errors []packages.Error
+}
+
+func (e *PackageError) Error() string {
+	var b strings.Builder
+	fmt.Fprintln(&b, "Packages contain errors:")
+	for _, e := range e.Errors {
+		fmt.Fprintln(&b, e)
+	}
+	return b.String()
+}
+
 // fileExists checks if file path exists. Returns true
 // if the file exists or it cannot prove that it does
 // not exist. Otherwise, returns false.
diff --git a/internal/govulncheck/formatting.go b/cmd/govulncheck/formatting.go
similarity index 98%
rename from internal/govulncheck/formatting.go
rename to cmd/govulncheck/formatting.go
index c2254da..10814a4 100644
--- a/internal/govulncheck/formatting.go
+++ b/cmd/govulncheck/formatting.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package govulncheck
+package main
 
 import (
 	"bytes"
diff --git a/internal/govulncheck/formatting_test.go b/cmd/govulncheck/formatting_test.go
similarity index 98%
rename from internal/govulncheck/formatting_test.go
rename to cmd/govulncheck/formatting_test.go
index d8de82b..48fdd9e 100644
--- a/internal/govulncheck/formatting_test.go
+++ b/cmd/govulncheck/formatting_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package govulncheck
+package main
 
 import (
 	"bytes"
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 9cdd971..6f06b25 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -6,7 +6,6 @@
 
 import (
 	"context"
-	"errors"
 	"flag"
 	"fmt"
 	"os"
@@ -15,7 +14,10 @@
 
 	"golang.org/x/tools/go/buildutil"
 	"golang.org/x/tools/go/packages"
-	"golang.org/x/vuln/internal/govulncheck"
+	"golang.org/x/vuln/client"
+	"golang.org/x/vuln/exp/govulncheck"
+	gvc "golang.org/x/vuln/internal/govulncheck"
+	"golang.org/x/vuln/vulncheck"
 )
 
 var (
@@ -32,6 +34,11 @@
 	flag.Var(&tagsFlag, "tags", "comma-separated `list` of build tags")
 }
 
+const (
+	envGOVULNDB = "GOVULNDB"
+	vulndbHost  = "https://vuln.go.dev"
+)
+
 func main() {
 	flag.Usage = func() {
 		fmt.Fprint(os.Stderr, `usage:
@@ -53,53 +60,103 @@
 
 	patterns := flag.Args()
 
-	mode := govulncheck.AnalysisTypeSource
+	sourceAnalysis := true
 	if len(patterns) == 1 && isFile(patterns[0]) {
-		mode = govulncheck.AnalysisTypeBinary
+		sourceAnalysis = false
 	}
-	validateFlags(mode)
+	validateFlags(sourceAnalysis)
 
-	outputType := govulncheck.OutputTypeText
-	if *jsonFlag {
-		outputType = govulncheck.OutputTypeJSON
-	} else if *verboseFlag {
-		outputType = govulncheck.OutputTypeVerbose
-	}
-
-	var buildFlags []string
-	if tagsFlag != nil {
-		buildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))}
-	}
-
-	ctx := context.Background()
-	_, err := govulncheck.LegacyRun(ctx, govulncheck.LegacyConfig{
-		AnalysisType: mode,
-		OutputType:   outputType,
-		Patterns:     patterns,
-		SourceLoadConfig: &packages.Config{
-			Dir:        filepath.FromSlash(dirFlag),
-			Tests:      *testFlag,
-			BuildFlags: buildFlags,
-		},
-	})
-	if outputType == govulncheck.OutputTypeJSON {
-		// The current behavior is to not print any errors.
-		return
-	}
-	if errors.Is(err, govulncheck.ErrContainsVulnerabilties) {
-		// This follows the style from
-		// golang.org/x/tools/go/analysis/singlechecker,
-		// which fails with 3 if there are findings (in this case, vulns).
-		os.Exit(3)
-	}
-	if err != nil {
+	if err := doGovulncheck(patterns, sourceAnalysis); err != nil {
 		die(fmt.Sprintf("govulncheck: %v", err))
 	}
 }
 
-func validateFlags(mode string) {
-	switch mode {
-	case govulncheck.AnalysisTypeBinary:
+// doGovulncheck performs main govulncheck functionality and exits the
+// program upon success with an appropriate exit status. Otherwise,
+// returns an error.
+func doGovulncheck(patterns []string, sourceAnalysis bool) error {
+	ctx := context.Background()
+	dir := filepath.FromSlash(dirFlag)
+
+	dbs := []string{vulndbHost}
+	if db := os.Getenv(envGOVULNDB); db != "" {
+		dbs = strings.Split(db, ",")
+	}
+	dbClient, err := client.NewClient(dbs, client.Options{
+		HTTPCache: govulncheck.DefaultCache(),
+	})
+	if err != nil {
+		return err
+	}
+
+	if !*jsonFlag {
+		// Print intro message when in text or verbose mode
+		fmt.Println(introMessage)
+	}
+
+	// config GoVersion is "", which means use current
+	// Go version at path.
+	cfg := &govulncheck.Config{Client: dbClient}
+	var res *govulncheck.Result
+	if sourceAnalysis {
+		pkgs, err := loadPackages(patterns, dir)
+		if err != nil {
+			// Try to provide a meaningful and actionable error message.
+			if !fileExists(filepath.Join(dir, "go.mod")) {
+				return ErrNoGoMod
+			}
+			if !fileExists(filepath.Join(dir, "go.sum")) {
+				return ErrNoGoSum
+			}
+			if isGoVersionMismatchError(err) {
+				return fmt.Errorf("%v\n\n%v", ErrGoVersionMismatch, err)
+			}
+			return err
+		}
+		res, err = govulncheck.Source(ctx, cfg, pkgs)
+	} else {
+		f, err := os.Open(patterns[0])
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+		res, err = gvc.Binary(ctx, cfg, f)
+	}
+	if err != nil {
+		return err
+	}
+
+	if *jsonFlag {
+		// Following golang.org/x/tools/go/analysis/singlechecker,
+		// return 0 exit code in -json mode.
+		if err := printJSON(res); err != nil {
+			return err
+		}
+		os.Exit(0)
+	}
+
+	printText(res, *verboseFlag, sourceAnalysis)
+	// Return exit status -3 if some vulnerabilities are actually
+	// called in source mode or just present in binary mode.
+	//
+	// This follows the style from
+	// golang.org/x/tools/go/analysis/singlechecker,
+	// which fails with 3 if there are some findings.
+	if sourceAnalysis {
+		for _, v := range res.Vulns {
+			if v.IsCalled() {
+				os.Exit(3)
+			}
+		}
+	} else if len(res.Vulns) > 0 {
+		os.Exit(3)
+	}
+	os.Exit(0)
+	return nil
+}
+
+func validateFlags(source bool) {
+	if !source {
 		if *testFlag {
 			die("govulncheck: the -test flag is invalid for binaries")
 		}
@@ -121,3 +178,34 @@
 	fmt.Fprintf(os.Stderr, format+"\n", args...)
 	os.Exit(1)
 }
+
+// loadPackages loads the packages matching patterns at dir using build tags
+// provided by tagsFlag. Uses load mode needed for vulncheck analysis. If the
+// packages contain errors, a PackageError is returned containing a list of
+// the errors, along with the packages themselves.
+func loadPackages(patterns []string, dir string) ([]*vulncheck.Package, error) {
+	var buildFlags []string
+	if tagsFlag != nil {
+		buildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))}
+	}
+
+	cfg := &packages.Config{Dir: dir, Tests: *testFlag}
+	cfg.Mode |= packages.NeedName | packages.NeedImports | packages.NeedTypes |
+		packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
+		packages.NeedModule
+	cfg.BuildFlags = buildFlags
+
+	pkgs, err := packages.Load(cfg, patterns...)
+	vpkgs := vulncheck.Convert(pkgs)
+	if err != nil {
+		return nil, err
+	}
+	var perrs []packages.Error
+	packages.Visit(pkgs, nil, func(p *packages.Package) {
+		perrs = append(perrs, p.Errors...)
+	})
+	if len(perrs) > 0 {
+		err = &PackageError{perrs}
+	}
+	return vpkgs, err
+}
diff --git a/cmd/govulncheck/message.go b/cmd/govulncheck/message.go
index 61ff8af..07e8977 100644
--- a/cmd/govulncheck/message.go
+++ b/cmd/govulncheck/message.go
@@ -4,38 +4,15 @@
 
 package main
 
-import (
-	"fmt"
-
-	"golang.org/x/vuln/internal/govulncheck"
-)
-
 const (
-	noGoModErrorMessage = `govulncheck only works Go with modules. To make your project a module, run go mod init.
+	introMessage = `govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
-See https://go.dev/doc/modules/managing-dependencies for more information.`
+Scanning for dependencies with known vulnerabilities...`
 
-	noGoSumErrorMessage = `Your module is missing a go.sum file. Try running go mod tidy.
+	informationalMessage = `=== Informational ===
 
-See https://go.dev/doc/modules/managing-dependencies for more information.`
-
-	goVersionMismatchErrorMessage = `Loading packages failed, possibly due to a mismatch between the Go version
-used to build govulncheck and the Go version on PATH. Consider rebuilding
-govulncheck with the current Go version.`
+The vulnerabilities below are in packages that you import, but your code
+doesn't appear to call any vulnerable functions. You may not need to take any
+action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
+for details.`
 )
-
-var errToMessage = map[error]string{
-	govulncheck.ErrNoGoMod:             noGoModErrorMessage,
-	govulncheck.ErrNoGoSum:             noGoSumErrorMessage,
-	govulncheck.ErrGoVersionMismatch:   goVersionMismatchErrorMessage,
-	govulncheck.ErrInvalidAnalysisType: "",
-	govulncheck.ErrInvalidOutputType:   "",
-}
-
-func messageForError(err error) (out string) {
-	msg, ok := errToMessage[err]
-	if !ok {
-		return ""
-	}
-	return fmt.Sprintf("govulncheck: %v\n\n%s", err, msg)
-}
diff --git a/internal/govulncheck/print.go b/cmd/govulncheck/print.go
similarity index 91%
rename from internal/govulncheck/print.go
rename to cmd/govulncheck/print.go
index f503661..d3e4be5 100644
--- a/internal/govulncheck/print.go
+++ b/cmd/govulncheck/print.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package govulncheck
+package main
 
 import (
 	"encoding/json"
@@ -12,10 +12,12 @@
 	"strings"
 
 	"golang.org/x/exp/maps"
+	"golang.org/x/vuln/exp/govulncheck"
+	"golang.org/x/vuln/internal"
 	"golang.org/x/vuln/osv"
 )
 
-func printJSON(r *Result) error {
+func printJSON(r *govulncheck.Result) error {
 	b, err := json.MarshalIndent(r, "", "\t")
 	if err != nil {
 		return err
@@ -30,10 +32,10 @@
 	lineLength = 55
 )
 
-func printText(r *Result, verbose, source bool) {
+func printText(r *govulncheck.Result, verbose, source bool) {
 	// unaffected are (imported) OSVs none of
 	// which vulnerabilities are called.
-	var unaffected []*Vuln
+	var unaffected []*govulncheck.Vuln
 	uniqueVulns := 0
 	for _, v := range r.Vulns {
 		if !source || v.IsCalled() {
@@ -117,7 +119,7 @@
 `, idx, id, indent(details, 2), callstack, found, fixed, platforms, id)
 }
 
-func defaultCallStacks(css []CallStack) string {
+func defaultCallStacks(css []govulncheck.CallStack) string {
 	var summaries []string
 	for _, cs := range css {
 		summaries = append(summaries, cs.Summary)
@@ -137,15 +139,15 @@
 	return b.String()
 }
 
-func verboseCallStacks(css []CallStack) string {
+func verboseCallStacks(css []govulncheck.CallStack) string {
 	// Display one full call stack for each vuln.
 	i := 1
 	var b strings.Builder
 	for _, cs := range css {
 		b.WriteString(fmt.Sprintf("#%d: for function %s\n", i, cs.Symbol))
 		for _, e := range cs.Frames {
-			b.WriteString(fmt.Sprintf("  %s\n", funcName(e)))
-			if pos := AbsRelShorter(funcPos(e)); pos != "" {
+			b.WriteString(fmt.Sprintf("  %s\n", e.Name()))
+			if pos := internal.AbsRelShorter(e.Pos()); pos != "" {
 				b.WriteString(fmt.Sprintf("      %s\n", pos))
 			}
 		}
diff --git a/cmd/govulncheck/print_test.go b/cmd/govulncheck/print_test.go
new file mode 100644
index 0000000..2f8b089
--- /dev/null
+++ b/cmd/govulncheck/print_test.go
@@ -0,0 +1,88 @@
+// 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 main
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/vuln/osv"
+)
+
+func TestPlatforms(t *testing.T) {
+	for _, test := range []struct {
+		entry *osv.Entry
+		want  string
+	}{
+		{
+			entry: &osv.Entry{ID: "All"},
+			want:  "",
+		},
+		{
+			entry: &osv.Entry{
+				ID: "one-import",
+				Affected: []osv.Affected{{
+					Package: osv.Package{Name: "golang.org/vmod"},
+					Ranges:  osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
+					EcosystemSpecific: osv.EcosystemSpecific{
+						Imports: []osv.EcosystemSpecificImport{{
+							GOOS:   []string{"windows", "linux"},
+							GOARCH: []string{"amd64", "wasm"},
+						}},
+					},
+				}},
+			},
+			want: "linux/amd64, linux/wasm, windows/amd64, windows/wasm",
+		},
+		{
+			entry: &osv.Entry{
+				ID: "two-imports",
+				Affected: []osv.Affected{{
+					Package: osv.Package{Name: "golang.org/vmod"},
+					Ranges:  osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
+					EcosystemSpecific: osv.EcosystemSpecific{
+						Imports: []osv.EcosystemSpecificImport{
+							{
+								GOOS:   []string{"windows"},
+								GOARCH: []string{"amd64"},
+							},
+							{
+								GOOS:   []string{"linux"},
+								GOARCH: []string{"amd64"},
+							},
+						},
+					},
+				}},
+			},
+			want: "linux/amd64, windows/amd64",
+		},
+	} {
+		t.Run(test.entry.ID, func(t *testing.T) {
+			got := platforms(test.entry)
+			if got != test.want {
+				t.Errorf("got %q, want %q", got, test.want)
+			}
+		})
+	}
+}
+
+func TestIndent(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		s    string
+		n    int
+		want string
+	}{
+		{"short", "hello", 2, "  hello"},
+		{"multi", "mulit\nline\nstring", 1, " mulit\n line\n string"},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			got := indent(test.s, test.n)
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Fatalf("mismatch (-want, +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/exp/govulncheck/govulncheck.go b/exp/govulncheck/govulncheck.go
index ee36839..d09b0d0 100644
--- a/exp/govulncheck/govulncheck.go
+++ b/exp/govulncheck/govulncheck.go
@@ -7,8 +7,13 @@
 
 import "golang.org/x/vuln/internal/govulncheck"
 
-// Source reports vulnerabilities that affect the analyzed packages.
-var Source = govulncheck.Source
+var (
+	// Source reports vulnerabilities that affect the analyzed packages.
+	Source = govulncheck.Source
+
+	// DefaultCache constructs cache for a vulnerability database client.
+	DefaultCache = govulncheck.DefaultCache
+)
 
 type (
 	// Config is the configuration for Main.
diff --git a/internal/govulncheck/filepath.go b/internal/filepath.go
similarity index 97%
rename from internal/govulncheck/filepath.go
rename to internal/filepath.go
index 7f447fd..870af76 100644
--- a/internal/govulncheck/filepath.go
+++ b/internal/filepath.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package govulncheck
+package internal
 
 import (
 	"path/filepath"
diff --git a/internal/govulncheck/filepath_test.go b/internal/filepath_test.go
similarity index 97%
rename from internal/govulncheck/filepath_test.go
rename to internal/filepath_test.go
index 3f59ab4..1f64d77 100644
--- a/internal/govulncheck/filepath_test.go
+++ b/internal/filepath_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package govulncheck
+package internal
 
 import (
 	"os"
diff --git a/internal/govulncheck/config.go b/internal/govulncheck/config.go
deleted file mode 100644
index 33ac937..0000000
--- a/internal/govulncheck/config.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// 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
-
-import "golang.org/x/tools/go/packages"
-
-const (
-	// AnalysisTypeBinary is used for binary analysis with vulncheck.Binary.
-	AnalysisTypeBinary = "binary"
-
-	// AnalysisTypeSource is used for source code analysis with vulncheck.Source.
-	AnalysisTypeSource = "source"
-)
-
-const (
-	// OutputTypeText is the default output type for `govulncheck`.
-	OutputTypeText = "text"
-
-	//  OutputTypeVerbose is the output type for `govulncheck -v`.
-	OutputTypeVerbose = "verbose"
-
-	// OutputTypeJSON is the output type for `govulncheck -json`, which will print
-	// the JSON-encoded vulncheck.Result.
-	OutputTypeJSON = "json"
-)
-
-const (
-	envGOVULNDB = "GOVULNDB"
-	vulndbHost  = "https://vuln.go.dev"
-)
-
-// LegacyConfig is the configuration for Main.
-type LegacyConfig struct {
-	// AnalysisType specifies the vulncheck analysis type.
-	AnalysisType string
-
-	// OutputType specifies the output format type.
-	OutputType 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
-
-	// GoVersion specifies the go version used when analyzing source code.
-	// The default is the version of the go command found from the PATH (Path).
-	GoVersion string
-}
diff --git a/internal/govulncheck/filter.go b/internal/govulncheck/filter.go
deleted file mode 100644
index b37d298..0000000
--- a/internal/govulncheck/filter.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// 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
-
-import (
-	"sort"
-
-	"golang.org/x/vuln/vulncheck"
-)
-
-// filterCalled returns vulnerabilities where the symbols are actually called.
-func filterCalled(r *vulncheck.Result) []*vulncheck.Vuln {
-	var vulns []*vulncheck.Vuln
-	for _, v := range r.Vulns {
-		if v.CallSink != 0 {
-			vulns = append(vulns, v)
-		}
-	}
-	sortVulns(vulns)
-	return vulns
-}
-
-// filterUnaffected returns vulnerabilities where no symbols are called,
-// grouped by module.
-func filterUnaffected(r *vulncheck.Result) []*vulncheck.Vuln {
-	// It is possible that the same vuln.OSV.ID has vuln.CallSink != 0
-	// for one symbol, but vuln.CallSink == 0 for a different one, so
-	// we need to filter out ones that have been called.
-	called := filterCalled(r)
-	calledIDs := map[string]bool{}
-	for _, vuln := range called {
-		calledIDs[vuln.OSV.ID] = true
-	}
-
-	idToVuln := map[string]*vulncheck.Vuln{}
-	for _, vuln := range r.Vulns {
-		if !calledIDs[vuln.OSV.ID] {
-			idToVuln[vuln.OSV.ID] = vuln
-		}
-	}
-	var output []*vulncheck.Vuln
-	for _, vuln := range idToVuln {
-		output = append(output, vuln)
-	}
-	sortVulns(output)
-	return output
-}
-
-func sortVulns(vulns []*vulncheck.Vuln) {
-	sort.Slice(vulns, func(i, j int) bool {
-		return vulns[i].OSV.ID > vulns[j].OSV.ID
-	})
-}
-
-func sortPackages(pkgs []*vulncheck.Package) {
-	sort.Slice(pkgs, func(i, j int) bool {
-		return pkgs[i].PkgPath < pkgs[j].PkgPath
-	})
-	for _, pkg := range pkgs {
-		sort.Slice(pkg.Imports, func(i, j int) bool {
-			return pkg.Imports[i].PkgPath < pkg.Imports[j].PkgPath
-		})
-	}
-}
diff --git a/internal/govulncheck/legacy_run.go b/internal/govulncheck/legacy_run.go
deleted file mode 100644
index b69bb68..0000000
--- a/internal/govulncheck/legacy_run.go
+++ /dev/null
@@ -1,137 +0,0 @@
-// 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
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"path/filepath"
-	"strings"
-
-	"golang.org/x/tools/go/packages"
-	"golang.org/x/vuln/client"
-	"golang.org/x/vuln/vulncheck"
-)
-
-// LegacyRun is the main function for the govulncheck command line tool.
-//
-// TODO: inline into cmd/govulncheck. This will effectively remove the
-// need for having additional (Legacy)Config.
-func LegacyRun(ctx context.Context, lcfg LegacyConfig) (*Result, error) {
-	dbs := []string{vulndbHost}
-	if db := os.Getenv(envGOVULNDB); db != "" {
-		dbs = strings.Split(db, ",")
-	}
-	dbClient, err := client.NewClient(dbs, client.Options{
-		HTTPCache: DefaultCache(),
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	format := lcfg.OutputType
-	if format == OutputTypeText || format == OutputTypeVerbose {
-		fmt.Println(introMessage)
-	}
-
-	cfg := &Config{Client: dbClient, GoVersion: lcfg.GoVersion}
-	var res *Result
-	switch lcfg.AnalysisType {
-	case AnalysisTypeBinary:
-		f, err := os.Open(lcfg.Patterns[0])
-		if err != nil {
-			return nil, err
-		}
-		defer f.Close()
-		res, err = Binary(ctx, cfg, f)
-	case AnalysisTypeSource:
-		pkgs, err := loadPackages(lcfg)
-		if err != nil {
-			// Try to provide a meaningful and actionable error message.
-			if !fileExists(filepath.Join(lcfg.SourceLoadConfig.Dir, "go.mod")) {
-				return nil, ErrNoGoMod
-			}
-			if !fileExists(filepath.Join(lcfg.SourceLoadConfig.Dir, "go.sum")) {
-				return nil, ErrNoGoSum
-			}
-			if isGoVersionMismatchError(err) {
-				return nil, fmt.Errorf("%v\n\n%v", ErrGoVersionMismatch, err)
-			}
-			return nil, err
-		}
-		res, err = Source(ctx, cfg, pkgs)
-	default:
-		return nil, fmt.Errorf("%w: %s", ErrInvalidAnalysisType, lcfg.AnalysisType)
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	switch lcfg.OutputType {
-	case OutputTypeJSON:
-		// Following golang.org/x/tools/go/analysis/singlechecker,
-		// return 0 exit code in -json mode.
-		if err := printJSON(res); err != nil {
-			return nil, err
-		}
-		return res, nil
-	case OutputTypeText, OutputTypeVerbose:
-		source := lcfg.AnalysisType == AnalysisTypeSource
-		printText(res, lcfg.OutputType == OutputTypeVerbose, source)
-		// Return error if some vulnerabilities are actually called.
-		if source {
-			for _, v := range res.Vulns {
-				if v.IsCalled() {
-					return nil, ErrContainsVulnerabilties
-				}
-			}
-		} else if len(res.Vulns) > 0 {
-			return nil, ErrContainsVulnerabilties
-		}
-		return res, nil
-	default:
-		return nil, fmt.Errorf("%w: %s", ErrInvalidOutputType, lcfg.OutputType)
-	}
-}
-
-// A PackageError contains errors from loading a set of packages.
-type PackageError struct {
-	Errors []packages.Error
-}
-
-func (e *PackageError) Error() string {
-	var b strings.Builder
-	fmt.Fprintln(&b, "Packages contain errors:")
-	for _, e := range e.Errors {
-		fmt.Fprintln(&b, e)
-	}
-	return b.String()
-}
-
-// loadPackages loads the packages matching patterns using cfg, after setting
-// the cfg mode flags that vulncheck needs for analysis.
-// If the packages contain errors, a PackageError is returned containing a list of the errors,
-// along with the packages themselves.
-func loadPackages(cfg LegacyConfig) ([]*vulncheck.Package, error) {
-	patterns := cfg.Patterns
-	cfg.SourceLoadConfig.Mode |= packages.NeedName | packages.NeedImports | packages.NeedTypes |
-		packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
-		packages.NeedModule
-
-	pkgs, err := packages.Load(cfg.SourceLoadConfig, patterns...)
-	vpkgs := vulncheck.Convert(pkgs)
-	if err != nil {
-		return nil, err
-	}
-	var perrs []packages.Error
-	packages.Visit(pkgs, nil, func(p *packages.Package) {
-		perrs = append(perrs, p.Errors...)
-	})
-	if len(perrs) > 0 {
-		err = &PackageError{perrs}
-	}
-	return vpkgs, err
-}
diff --git a/internal/govulncheck/message.go b/internal/govulncheck/message.go
deleted file mode 100644
index 737d182..0000000
--- a/internal/govulncheck/message.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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
-
-const (
-	introMessage = `govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
-
-Scanning for dependencies with known vulnerabilities...`
-
-	informationalMessage = `=== Informational ===
-
-The vulnerabilities below are in packages that you import, but your code
-doesn't appear to call any vulnerable functions. You may not need to take any
-action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
-for details.`
-)
diff --git a/internal/govulncheck/result.go b/internal/govulncheck/result.go
index 9e5a991..b0a3128 100644
--- a/internal/govulncheck/result.go
+++ b/internal/govulncheck/result.go
@@ -6,7 +6,9 @@
 package govulncheck
 
 import (
+	"fmt"
 	"go/token"
+	"strings"
 
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/vuln/client"
@@ -30,11 +32,6 @@
 	//
 	// By default, GoVersion is the go command version found from the PATH.
 	GoVersion string
-
-	// Verbosity controls the stdout and stderr output when running Source.
-	//
-	// TODO(https://go.dev/issue/56042): make this an enum.
-	Verbosity string
 }
 
 // Result is the result of executing Source or Binary.
@@ -158,3 +155,24 @@
 	// A Position is valid if the line number is > 0.
 	Position token.Position
 }
+
+// Name returns the full qualified function name from sf,
+// adjusted to remove pointer annotations.
+func (sf *StackFrame) Name() string {
+	var n string
+	if sf.RecvType == "" {
+		n = fmt.Sprintf("%s.%s", sf.PkgPath, sf.FuncName)
+	} else {
+		n = fmt.Sprintf("%s.%s", sf.RecvType, sf.FuncName)
+	}
+	return strings.TrimPrefix(n, "*")
+}
+
+// Pos returns the position of the call in sf as string.
+// If position is not available, return "".
+func (sf *StackFrame) Pos() string {
+	if sf.Position.IsValid() {
+		return sf.Position.String()
+	}
+	return ""
+}
diff --git a/internal/govulncheck/run_test.go b/internal/govulncheck/run_test.go
index 4a6481d..bd93c57 100644
--- a/internal/govulncheck/run_test.go
+++ b/internal/govulncheck/run_test.go
@@ -108,82 +108,6 @@
 	}
 }
 
-func TestPlatforms(t *testing.T) {
-	for _, test := range []struct {
-		entry *osv.Entry
-		want  string
-	}{
-		{
-			entry: &osv.Entry{ID: "All"},
-			want:  "",
-		},
-		{
-			entry: &osv.Entry{
-				ID: "one-import",
-				Affected: []osv.Affected{{
-					Package: osv.Package{Name: "golang.org/vmod"},
-					Ranges:  osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
-					EcosystemSpecific: osv.EcosystemSpecific{
-						Imports: []osv.EcosystemSpecificImport{{
-							GOOS:   []string{"windows", "linux"},
-							GOARCH: []string{"amd64", "wasm"},
-						}},
-					},
-				}},
-			},
-			want: "linux/amd64, linux/wasm, windows/amd64, windows/wasm",
-		},
-		{
-			entry: &osv.Entry{
-				ID: "two-imports",
-				Affected: []osv.Affected{{
-					Package: osv.Package{Name: "golang.org/vmod"},
-					Ranges:  osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
-					EcosystemSpecific: osv.EcosystemSpecific{
-						Imports: []osv.EcosystemSpecificImport{
-							{
-								GOOS:   []string{"windows"},
-								GOARCH: []string{"amd64"},
-							},
-							{
-								GOOS:   []string{"linux"},
-								GOARCH: []string{"amd64"},
-							},
-						},
-					},
-				}},
-			},
-			want: "linux/amd64, windows/amd64",
-		},
-	} {
-		t.Run(test.entry.ID, func(t *testing.T) {
-			got := platforms(test.entry)
-			if got != test.want {
-				t.Errorf("got %q, want %q", got, test.want)
-			}
-		})
-	}
-}
-
-func TestIndent(t *testing.T) {
-	for _, test := range []struct {
-		name string
-		s    string
-		n    int
-		want string
-	}{
-		{"short", "hello", 2, "  hello"},
-		{"multi", "mulit\nline\nstring", 1, " mulit\n line\n string"},
-	} {
-		t.Run(test.name, func(t *testing.T) {
-			got := indent(test.s, test.n)
-			if diff := cmp.Diff(test.want, got); diff != "" {
-				t.Fatalf("mismatch (-want, +got):\n%s", diff)
-			}
-		})
-	}
-}
-
 func TestUniqueCallStack(t *testing.T) {
 	a := &vulncheck.FuncNode{Name: "A"}
 	b := &vulncheck.FuncNode{Name: "B"}
diff --git a/internal/govulncheck/util.go b/internal/govulncheck/util.go
index 3c750a3..f713444 100644
--- a/internal/govulncheck/util.go
+++ b/internal/govulncheck/util.go
@@ -82,7 +82,6 @@
 		return topPkgs[e.PkgPath]
 	})
 	if iTop < 0 {
-		print("1\n")
 		return ""
 	}
 	// Find the highest function in the vulnerable package that is below iTop.
@@ -90,21 +89,20 @@
 		return e.PkgPath == vulnPkg
 	})
 	if iVuln < 0 {
-		print("2\n")
 		return ""
 	}
 	iVuln += iTop + 1 // adjust for slice in call to highest.
-	topName := funcName(cs.Frames[iTop])
-	topPos := AbsRelShorter(funcPos(cs.Frames[iTop]))
+	topName := cs.Frames[iTop].Name()
+	topPos := internal.AbsRelShorter(cs.Frames[iTop].Pos())
 	if topPos != "" {
 		topPos += ": "
 	}
-	vulnName := funcName(cs.Frames[iVuln])
+	vulnName := cs.Frames[iVuln].Name()
 	if iVuln == iTop+1 {
 		return fmt.Sprintf("%s%s calls %s", topPos, topName, vulnName)
 	}
 	return fmt.Sprintf("%s%s calls %s, which eventually calls %s",
-		topPos, topName, funcName(cs.Frames[iTop+1]), vulnName)
+		topPos, topName, cs.Frames[iTop+1].Name(), vulnName)
 }
 
 // highest returns the highest (one with the smallest index) entry in the call
@@ -142,24 +140,3 @@
 	}
 	return s
 }
-
-// funcName returns the full qualified function name from fn,
-// adjusted to remove pointer annotations.
-func funcName(sf *StackFrame) string {
-	var n string
-	if sf.RecvType == "" {
-		n = fmt.Sprintf("%s.%s", sf.PkgPath, sf.FuncName)
-	} else {
-		n = fmt.Sprintf("%s.%s", sf.RecvType, sf.FuncName)
-	}
-	return strings.TrimPrefix(n, "*")
-}
-
-// funcPos returns the position of the call in sf as string.
-// If position is not available, return "".
-func funcPos(sf *StackFrame) string {
-	if sf.Position.IsValid() {
-		return sf.Position.String()
-	}
-	return ""
-}