all: cleanup govulncheck packages

This CL does not add any new code. It simply

- renames files and moves code accordingly
- makes private those symbols that do not need to be public anymore

For golang/go#56042

Change-Id: I827a84540f0c7200f4d68bf59ce1f6a59a52877f
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/448775
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/govulncheck/errors.go b/cmd/govulncheck/errors.go
index 04fe666..d6506e0 100644
--- a/cmd/govulncheck/errors.go
+++ b/cmd/govulncheck/errors.go
@@ -15,29 +15,29 @@
 )
 
 var (
-	// ErrErrGoVersionMismatch is used to indicate that there is a mismatch between
+	// 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
+	errGoVersionMismatch = errors.New(`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.`)
 
-	// ErrNoGoSum indicates that a go.mod file was not found in this module.
-	ErrNoGoMod = errors.New(`no go.mod file
+	// errNoGoSum indicates that a go.mod file was not found in this module.
+	errNoGoMod = errors.New(`no go.mod file
 
 govulncheck only works Go with modules. Try navigating to your module directory.
 Otherwise, run go mod init to make your project a module.
 
 See https://go.dev/doc/modules/managing-dependencies for more information.`)
 
-	// ErrNoGoSum indicates that a go.sum file was not found in this module.
-	ErrNoGoSum = errors.New(`no go.sum file
+	// errNoGoSum indicates that a go.sum file was not found in this module.
+	errNoGoSum = errors.New(`no go.sum file
 
 Your module is missing a go.sum file. Try running go mod tidy.
 
 See https://go.dev/doc/modules/managing-dependencies for more information.`)
 
-	// ErrNoModVersion indicates that govulncheck cannot access module version information.
-	ErrNoModVersion = errors.New(`no module version information
+	// errNoModVersion indicates that govulncheck cannot access module version information.
+	errNoModVersion = errors.New(`no module version information
 
 This can happen when running govulncheck in GOPATH mode. govulncheck needs module
 versions to correctly identify vulnerabilities.
@@ -45,12 +45,12 @@
 See https://go.dev/doc/modules/managing-dependencies for more information.`)
 )
 
-// A PackageError contains errors from loading a set of packages.
-type PackageError struct {
+// packageError contains errors from loading a set of packages.
+type packageError struct {
 	Errors []packages.Error
 }
 
-func (e *PackageError) Error() string {
+func (e *packageError) Error() string {
 	var b strings.Builder
 	fmt.Fprintln(&b, "Packages contain errors:")
 	for _, e := range e.Errors {
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 6f06b25..1df8011 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -103,13 +103,13 @@
 		if err != nil {
 			// Try to provide a meaningful and actionable error message.
 			if !fileExists(filepath.Join(dir, "go.mod")) {
-				return ErrNoGoMod
+				return errNoGoMod
 			}
 			if !fileExists(filepath.Join(dir, "go.sum")) {
-				return ErrNoGoSum
+				return errNoGoSum
 			}
 			if isGoVersionMismatchError(err) {
-				return fmt.Errorf("%v\n\n%v", ErrGoVersionMismatch, err)
+				return fmt.Errorf("%v\n\n%v", errGoVersionMismatch, err)
 			}
 			return err
 		}
@@ -181,7 +181,7 @@
 
 // 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
+// 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
@@ -205,7 +205,7 @@
 		perrs = append(perrs, p.Errors...)
 	})
 	if len(perrs) > 0 {
-		err = &PackageError{perrs}
+		err = &packageError{perrs}
 	}
 	return vpkgs, err
 }
diff --git a/internal/govulncheck/inits.go b/internal/govulncheck/callstacks.go
similarity index 64%
rename from internal/govulncheck/inits.go
rename to internal/govulncheck/callstacks.go
index ef18b05..f97e1bc 100644
--- a/internal/govulncheck/inits.go
+++ b/internal/govulncheck/callstacks.go
@@ -11,6 +11,7 @@
 	"strconv"
 	"strings"
 
+	"golang.org/x/vuln/internal"
 	"golang.org/x/vuln/vulncheck"
 )
 
@@ -114,24 +115,61 @@
 	return f.Name == "init" || strings.HasPrefix(f.Name, "init#")
 }
 
-// pkgMap creates a map from package paths to packages for all pkgs
-// and their transitive imports.
-func pkgMap(pkgs []*vulncheck.Package) map[string]*vulncheck.Package {
-	m := make(map[string]*vulncheck.Package)
-	var visit func(*vulncheck.Package)
-	visit = func(p *vulncheck.Package) {
-		if _, ok := m[p.PkgPath]; ok {
-			return
-		}
-		m[p.PkgPath] = p
+// summarizeCallStack returns a short description of the call stack.
+// It uses one of two forms, depending on what the lowest function F in topPkgs
+// calls:
+//   - If it calls a function V from the vulnerable package, then summarizeCallStack
+//     returns "F calls V".
+//   - If it calls a function G in some other package, which eventually calls V,
+//     it returns "F calls G, which eventually calls V".
+//
+// If it can't find any of these functions, summarizeCallStack returns the empty string.
+func summarizeCallStack(cs CallStack, topPkgs map[string]bool, vulnPkg string) string {
+	// Find the lowest function in the top packages.
+	iTop := lowest(cs.Frames, func(e *StackFrame) bool {
+		return topPkgs[e.PkgPath]
+	})
+	if iTop < 0 {
+		return ""
+	}
+	// Find the highest function in the vulnerable package that is below iTop.
+	iVuln := highest(cs.Frames[iTop+1:], func(e *StackFrame) bool {
+		return e.PkgPath == vulnPkg
+	})
+	if iVuln < 0 {
+		return ""
+	}
+	iVuln += iTop + 1 // adjust for slice in call to highest.
+	topName := cs.Frames[iTop].Name()
+	topPos := internal.AbsRelShorter(cs.Frames[iTop].Pos())
+	if topPos != "" {
+		topPos += ": "
+	}
+	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, cs.Frames[iTop+1].Name(), vulnName)
+}
 
-		for _, i := range p.Imports {
-			visit(i)
-		}
+// uniqueCallStack returns the first unique call stack among css, if any.
+// Unique means that the call stack does not go through symbols of vg.
+func uniqueCallStack(v *vulncheck.Vuln, css []vulncheck.CallStack, vg []*vulncheck.Vuln, r *vulncheck.Result) vulncheck.CallStack {
+	vulnFuncs := make(map[*vulncheck.FuncNode]bool)
+	for _, v := range vg {
+		vulnFuncs[r.Calls.Functions[v.CallSink]] = true
 	}
 
-	for _, p := range pkgs {
-		visit(p)
+	vulnFunc := r.Calls.Functions[v.CallSink]
+callstack:
+	for _, cs := range css {
+		for _, e := range cs {
+			if e.Function != vulnFunc && vulnFuncs[e.Function] {
+				continue callstack
+			}
+		}
+		return cs
 	}
-	return m
+	return nil
 }
diff --git a/internal/govulncheck/inits_test.go b/internal/govulncheck/callstacks_test.go
similarity index 63%
rename from internal/govulncheck/inits_test.go
rename to internal/govulncheck/callstacks_test.go
index 29134a0..8e6aef8 100644
--- a/internal/govulncheck/inits_test.go
+++ b/internal/govulncheck/callstacks_test.go
@@ -9,6 +9,7 @@
 	"fmt"
 	"path"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -18,6 +19,98 @@
 	"golang.org/x/vuln/vulncheck"
 )
 
+func TestUniqueCallStack(t *testing.T) {
+	a := &vulncheck.FuncNode{Name: "A"}
+	b := &vulncheck.FuncNode{Name: "B"}
+	v1 := &vulncheck.FuncNode{Name: "V1"}
+	v2 := &vulncheck.FuncNode{Name: "V2"}
+	v3 := &vulncheck.FuncNode{Name: "V3"}
+
+	vuln1 := &vulncheck.Vuln{Symbol: "V1", CallSink: 1}
+	vuln2 := &vulncheck.Vuln{Symbol: "V2", CallSink: 2}
+	vuln3 := &vulncheck.Vuln{Symbol: "V3", CallSink: 3}
+
+	vr := &vulncheck.Result{
+		Calls: &vulncheck.CallGraph{
+			Functions: map[int]*vulncheck.FuncNode{1: v1, 2: v2, 3: v3},
+		},
+		Vulns: []*vulncheck.Vuln{vuln1, vuln2, vuln3},
+	}
+
+	callStack := func(fs ...*vulncheck.FuncNode) vulncheck.CallStack {
+		var cs vulncheck.CallStack
+		for _, f := range fs {
+			cs = append(cs, vulncheck.StackEntry{Function: f})
+		}
+		return cs
+	}
+
+	// V1, V2, and V3 are vulnerable symbols
+	skip := []*vulncheck.Vuln{vuln1, vuln2, vuln3}
+	for _, test := range []struct {
+		vuln *vulncheck.Vuln
+		css  []vulncheck.CallStack
+		want vulncheck.CallStack
+	}{
+		// [A -> B -> V3 -> V1, A -> V1] ==> A -> V1 since the first stack goes through V3
+		{vuln1, []vulncheck.CallStack{callStack(a, b, v3, v1), callStack(a, v1)}, callStack(a, v1)},
+		// [A -> V1 -> V2] ==> nil since the only candidate call stack goes through V1
+		{vuln2, []vulncheck.CallStack{callStack(a, v1, v2)}, nil},
+		// [A -> V1 -> V3, A -> B -> v3] ==> A -> B -> V3 since the first stack goes through V1
+		{vuln3, []vulncheck.CallStack{callStack(a, v1, v3), callStack(a, b, v3)}, callStack(a, b, v3)},
+	} {
+		t.Run(test.vuln.Symbol, func(t *testing.T) {
+			got := uniqueCallStack(test.vuln, test.css, skip, vr)
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Fatalf("mismatch (-want, +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestSummarizeCallStack(t *testing.T) {
+	topPkgs := map[string]bool{"t1": true, "t2": true}
+	vulnPkg := "v"
+
+	for _, test := range []struct {
+		in, want string
+	}{
+		{"a.F", ""},
+		{"t1.F", ""},
+		{"v.V", ""},
+		{
+			"t1.F v.V",
+			"t1.F calls v.V",
+		},
+		{
+			"t1.F t2.G v.V1 v.v2",
+			"t2.G calls v.V1",
+		},
+		{
+			"t1.F x.Y t2.G a.H b.I c.J v.V",
+			"t2.G calls a.H, which eventually calls v.V",
+		},
+	} {
+		in := stringToCallStack(test.in)
+		got := summarizeCallStack(in, topPkgs, vulnPkg)
+		if got != test.want {
+			t.Errorf("%s:\ngot  %s\nwant %s", test.in, got, test.want)
+		}
+	}
+}
+
+func stringToCallStack(s string) CallStack {
+	var cs CallStack
+	for _, e := range strings.Fields(s) {
+		parts := strings.Split(e, ".")
+		cs.Frames = append(cs.Frames, &StackFrame{
+			PkgPath:  parts[0],
+			FuncName: parts[1],
+		})
+	}
+	return cs
+}
+
 // TestInits checks for correct positions of init functions
 // and their respective calls (see #51575).
 func TestInits(t *testing.T) {
diff --git a/internal/govulncheck/run.go b/internal/govulncheck/run.go
index dc5c962..bfdfaf1 100644
--- a/internal/govulncheck/run.go
+++ b/internal/govulncheck/run.go
@@ -94,7 +94,7 @@
 					Frames: stackFramesfromEntries(vcs),
 					Symbol: vv.Symbol,
 				}
-				cs.Summary = SummarizeCallStack(cs, topPkgs, p.Path)
+				cs.Summary = summarizeCallStack(cs, topPkgs, p.Path)
 				p.CallStacks = []CallStack{cs}
 			}
 		}
@@ -253,37 +253,3 @@
 	}
 	return frames
 }
-
-// uniqueCallStack returns the first unique call stack among css, if any.
-// Unique means that the call stack does not go through symbols of vg.
-func uniqueCallStack(v *vulncheck.Vuln, css []vulncheck.CallStack, vg []*vulncheck.Vuln, r *vulncheck.Result) vulncheck.CallStack {
-	vulnFuncs := make(map[*vulncheck.FuncNode]bool)
-	for _, v := range vg {
-		vulnFuncs[r.Calls.Functions[v.CallSink]] = true
-	}
-
-	vulnFunc := r.Calls.Functions[v.CallSink]
-callstack:
-	for _, cs := range css {
-		for _, e := range cs {
-			if e.Function != vulnFunc && vulnFuncs[e.Function] {
-				continue callstack
-			}
-		}
-		return cs
-	}
-	return nil
-}
-
-// moduleVersionMap builds a map from module paths to versions.
-func moduleVersionMap(mods []*vulncheck.Module) map[string]string {
-	moduleVersions := map[string]string{}
-	for _, m := range mods {
-		v := m.Version
-		if m.Replace != nil {
-			v = m.Replace.Version
-		}
-		moduleVersions[m.Path] = v
-	}
-	return moduleVersions
-}
diff --git a/internal/govulncheck/run_test.go b/internal/govulncheck/run_test.go
index bd93c57..0fabf89 100644
--- a/internal/govulncheck/run_test.go
+++ b/internal/govulncheck/run_test.go
@@ -4,155 +4,4 @@
 
 package govulncheck
 
-import (
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	"golang.org/x/vuln/osv"
-	"golang.org/x/vuln/vulncheck"
-)
-
-func TestLatestFixed(t *testing.T) {
-	for _, test := range []struct {
-		name string
-		in   []osv.Affected
-		want string
-	}{
-		{"empty", nil, ""},
-		{
-			"no semver",
-			[]osv.Affected{
-				{
-					Ranges: osv.Affects{
-						{
-							Type: osv.TypeGit,
-							Events: []osv.RangeEvent{
-								{Introduced: "v1.0.0", Fixed: "v1.2.3"},
-							},
-						}},
-				},
-			},
-			"",
-		},
-		{
-			"one",
-			[]osv.Affected{
-				{
-					Ranges: osv.Affects{
-						{
-							Type: osv.TypeSemver,
-							Events: []osv.RangeEvent{
-								{Introduced: "v1.0.0", Fixed: "v1.2.3"},
-							},
-						}},
-				},
-			},
-			"v1.2.3",
-		},
-		{
-			"several",
-			[]osv.Affected{
-				{
-					Ranges: osv.Affects{
-						{
-							Type: osv.TypeSemver,
-							Events: []osv.RangeEvent{
-								{Introduced: "v1.0.0", Fixed: "v1.2.3"},
-								{Introduced: "v1.5.0", Fixed: "v1.5.6"},
-							},
-						}},
-				},
-				{
-					Ranges: osv.Affects{
-						{
-							Type: osv.TypeSemver,
-							Events: []osv.RangeEvent{
-								{Introduced: "v1.3.0", Fixed: "v1.4.1"},
-							},
-						}},
-				},
-			},
-			"v1.5.6",
-		},
-		{
-			"no v prefix",
-			[]osv.Affected{
-				{
-					Ranges: osv.Affects{
-						{
-							Type: osv.TypeSemver,
-							Events: []osv.RangeEvent{
-								{Fixed: "1.17.2"},
-							},
-						}},
-				},
-				{
-					Ranges: osv.Affects{
-						{
-							Type: osv.TypeSemver,
-							Events: []osv.RangeEvent{
-								{Introduced: "1.18.0", Fixed: "1.18.4"},
-							},
-						}},
-				},
-			},
-			"1.18.4",
-		},
-	} {
-		t.Run(test.name, func(t *testing.T) {
-			got := LatestFixed(test.in)
-			if got != test.want {
-				t.Errorf("got %q, want %q", got, test.want)
-			}
-		})
-	}
-}
-
-func TestUniqueCallStack(t *testing.T) {
-	a := &vulncheck.FuncNode{Name: "A"}
-	b := &vulncheck.FuncNode{Name: "B"}
-	v1 := &vulncheck.FuncNode{Name: "V1"}
-	v2 := &vulncheck.FuncNode{Name: "V2"}
-	v3 := &vulncheck.FuncNode{Name: "V3"}
-
-	vuln1 := &vulncheck.Vuln{Symbol: "V1", CallSink: 1}
-	vuln2 := &vulncheck.Vuln{Symbol: "V2", CallSink: 2}
-	vuln3 := &vulncheck.Vuln{Symbol: "V3", CallSink: 3}
-
-	vr := &vulncheck.Result{
-		Calls: &vulncheck.CallGraph{
-			Functions: map[int]*vulncheck.FuncNode{1: v1, 2: v2, 3: v3},
-		},
-		Vulns: []*vulncheck.Vuln{vuln1, vuln2, vuln3},
-	}
-
-	callStack := func(fs ...*vulncheck.FuncNode) vulncheck.CallStack {
-		var cs vulncheck.CallStack
-		for _, f := range fs {
-			cs = append(cs, vulncheck.StackEntry{Function: f})
-		}
-		return cs
-	}
-
-	// V1, V2, and V3 are vulnerable symbols
-	skip := []*vulncheck.Vuln{vuln1, vuln2, vuln3}
-	for _, test := range []struct {
-		vuln *vulncheck.Vuln
-		css  []vulncheck.CallStack
-		want vulncheck.CallStack
-	}{
-		// [A -> B -> V3 -> V1, A -> V1] ==> A -> V1 since the first stack goes through V3
-		{vuln1, []vulncheck.CallStack{callStack(a, b, v3, v1), callStack(a, v1)}, callStack(a, v1)},
-		// [A -> V1 -> V2] ==> nil since the only candidate call stack goes through V1
-		{vuln2, []vulncheck.CallStack{callStack(a, v1, v2)}, nil},
-		// [A -> V1 -> V3, A -> B -> v3] ==> A -> B -> V3 since the first stack goes through V1
-		{vuln3, []vulncheck.CallStack{callStack(a, v1, v3), callStack(a, b, v3)}, callStack(a, b, v3)},
-	} {
-		t.Run(test.vuln.Symbol, func(t *testing.T) {
-			got := uniqueCallStack(test.vuln, test.css, skip, vr)
-			if diff := cmp.Diff(test.want, got); diff != "" {
-				t.Fatalf("mismatch (-want, +got):\n%s", diff)
-			}
-		})
-	}
-}
+// TODO: add tests
diff --git a/internal/govulncheck/util.go b/internal/govulncheck/util.go
index f713444..aba3adb 100644
--- a/internal/govulncheck/util.go
+++ b/internal/govulncheck/util.go
@@ -5,7 +5,6 @@
 package govulncheck
 
 import (
-	"fmt"
 	"strings"
 
 	"golang.org/x/mod/semver"
@@ -15,11 +14,9 @@
 	"golang.org/x/vuln/vulncheck"
 )
 
-// LatestFixed returns the latest fixed version in the list of affected ranges,
+// latestFixed returns the latest fixed version in the list of affected ranges,
 // or the empty string if there are no fixed versions.
-//
-// TODO: make private
-func LatestFixed(as []osv.Affected) string {
+func latestFixed(as []osv.Affected) string {
 	v := ""
 	for _, a := range as {
 		for _, r := range a.Ranges {
@@ -45,7 +42,7 @@
 }
 
 func fixedVersion(modulePath string, affected []osv.Affected) string {
-	fixed := LatestFixed(affected)
+	fixed := latestFixed(affected)
 	if fixed != "" {
 		fixed = versionString(modulePath, fixed)
 	}
@@ -65,46 +62,6 @@
 	return v
 }
 
-// SummarizeCallStack returns a short description of the call stack.
-// It uses one of two forms, depending on what the lowest function F in topPkgs
-// calls:
-//   - If it calls a function V from the vulnerable package, then summarizeCallStack
-//     returns "F calls V".
-//   - If it calls a function G in some other package, which eventually calls V,
-//     it returns "F calls G, which eventually calls V".
-//
-// If it can't find any of these functions, summarizeCallStack returns the empty string.
-//
-// TODO: make private
-func SummarizeCallStack(cs CallStack, topPkgs map[string]bool, vulnPkg string) string {
-	// Find the lowest function in the top packages.
-	iTop := lowest(cs.Frames, func(e *StackFrame) bool {
-		return topPkgs[e.PkgPath]
-	})
-	if iTop < 0 {
-		return ""
-	}
-	// Find the highest function in the vulnerable package that is below iTop.
-	iVuln := highest(cs.Frames[iTop+1:], func(e *StackFrame) bool {
-		return e.PkgPath == vulnPkg
-	})
-	if iVuln < 0 {
-		return ""
-	}
-	iVuln += iTop + 1 // adjust for slice in call to highest.
-	topName := cs.Frames[iTop].Name()
-	topPos := internal.AbsRelShorter(cs.Frames[iTop].Pos())
-	if topPos != "" {
-		topPos += ": "
-	}
-	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, cs.Frames[iTop+1].Name(), vulnName)
-}
-
 // highest returns the highest (one with the smallest index) entry in the call
 // stack for which f returns true.
 func highest(cs []*StackFrame, f func(e *StackFrame) bool) int {
@@ -127,10 +84,8 @@
 	return -1
 }
 
-// PkgPath returns the package path from fn.
-//
-// TODO: make private
-func PkgPath(fn *vulncheck.FuncNode) string {
+// pkgPath returns the package path from fn.
+func pkgPath(fn *vulncheck.FuncNode) string {
 	if fn.PkgPath != "" {
 		return fn.PkgPath
 	}
@@ -140,3 +95,38 @@
 	}
 	return s
 }
+
+// moduleVersionMap builds a map from module paths to versions.
+func moduleVersionMap(mods []*vulncheck.Module) map[string]string {
+	moduleVersions := map[string]string{}
+	for _, m := range mods {
+		v := m.Version
+		if m.Replace != nil {
+			v = m.Replace.Version
+		}
+		moduleVersions[m.Path] = v
+	}
+	return moduleVersions
+}
+
+// pkgMap creates a map from package paths to packages for all pkgs
+// and their transitive imports.
+func pkgMap(pkgs []*vulncheck.Package) map[string]*vulncheck.Package {
+	m := make(map[string]*vulncheck.Package)
+	var visit func(*vulncheck.Package)
+	visit = func(p *vulncheck.Package) {
+		if _, ok := m[p.PkgPath]; ok {
+			return
+		}
+		m[p.PkgPath] = p
+
+		for _, i := range p.Imports {
+			visit(i)
+		}
+	}
+
+	for _, p := range pkgs {
+		visit(p)
+	}
+	return m
+}
diff --git a/internal/govulncheck/util_test.go b/internal/govulncheck/util_test.go
index 6bd4c46..c2a8711 100644
--- a/internal/govulncheck/util_test.go
+++ b/internal/govulncheck/util_test.go
@@ -5,9 +5,9 @@
 package govulncheck
 
 import (
-	"strings"
 	"testing"
 
+	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
 
@@ -29,52 +29,105 @@
 			"a.com/b",
 		},
 	} {
-		got := PkgPath(&test.in)
+		got := pkgPath(&test.in)
 		if got != test.want {
 			t.Errorf("%+v: got %q, want %q", test.in, got, test.want)
 		}
 	}
 }
 
-func TestSummarizeCallStack(t *testing.T) {
-	topPkgs := map[string]bool{"t1": true, "t2": true}
-	vulnPkg := "v"
-
+func TestLatestFixed(t *testing.T) {
 	for _, test := range []struct {
-		in, want string
+		name string
+		in   []osv.Affected
+		want string
 	}{
-		{"a.F", ""},
-		{"t1.F", ""},
-		{"v.V", ""},
+		{"empty", nil, ""},
 		{
-			"t1.F v.V",
-			"t1.F calls v.V",
+			"no semver",
+			[]osv.Affected{
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeGit,
+							Events: []osv.RangeEvent{
+								{Introduced: "v1.0.0", Fixed: "v1.2.3"},
+							},
+						}},
+				},
+			},
+			"",
 		},
 		{
-			"t1.F t2.G v.V1 v.v2",
-			"t2.G calls v.V1",
+			"one",
+			[]osv.Affected{
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeSemver,
+							Events: []osv.RangeEvent{
+								{Introduced: "v1.0.0", Fixed: "v1.2.3"},
+							},
+						}},
+				},
+			},
+			"v1.2.3",
 		},
 		{
-			"t1.F x.Y t2.G a.H b.I c.J v.V",
-			"t2.G calls a.H, which eventually calls v.V",
+			"several",
+			[]osv.Affected{
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeSemver,
+							Events: []osv.RangeEvent{
+								{Introduced: "v1.0.0", Fixed: "v1.2.3"},
+								{Introduced: "v1.5.0", Fixed: "v1.5.6"},
+							},
+						}},
+				},
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeSemver,
+							Events: []osv.RangeEvent{
+								{Introduced: "v1.3.0", Fixed: "v1.4.1"},
+							},
+						}},
+				},
+			},
+			"v1.5.6",
+		},
+		{
+			"no v prefix",
+			[]osv.Affected{
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeSemver,
+							Events: []osv.RangeEvent{
+								{Fixed: "1.17.2"},
+							},
+						}},
+				},
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeSemver,
+							Events: []osv.RangeEvent{
+								{Introduced: "1.18.0", Fixed: "1.18.4"},
+							},
+						}},
+				},
+			},
+			"1.18.4",
 		},
 	} {
-		in := stringToCallStack(test.in)
-		got := SummarizeCallStack(in, topPkgs, vulnPkg)
-		if got != test.want {
-			t.Errorf("%s:\ngot  %s\nwant %s", test.in, got, test.want)
-		}
-	}
-}
-
-func stringToCallStack(s string) CallStack {
-	var cs CallStack
-	for _, e := range strings.Fields(s) {
-		parts := strings.Split(e, ".")
-		cs.Frames = append(cs.Frames, &StackFrame{
-			PkgPath:  parts[0],
-			FuncName: parts[1],
+		t.Run(test.name, func(t *testing.T) {
+			got := latestFixed(test.in)
+			if got != test.want {
+				t.Errorf("got %q, want %q", got, test.want)
+			}
 		})
 	}
-	return cs
 }