cmd/govulncheck: move more functions to internal package

Move more govulncheck functionality to a separate package
so gopls can use it.

Change-Id: I48c86935f23cb9fe54eb0897f32d8be29eece4c3
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/406937
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/cmd/govulncheck/html.go b/cmd/govulncheck/html.go
index 090a464..2f7b678 100644
--- a/cmd/govulncheck/html.go
+++ b/cmd/govulncheck/html.go
@@ -22,7 +22,7 @@
 
 func html(w io.Writer, r *vulncheck.Result, ci *govulncheck.CallInfo) error {
 	tmpl, err := template.New("govulncheck.tmpl").Funcs(template.FuncMap{
-		"funcName": funcName,
+		"funcName": govulncheck.FuncName,
 	}).ParseFS(staticContent, "static/govulncheck.tmpl")
 	if err != nil {
 		return err
@@ -50,7 +50,7 @@
 			ID:             v0.OSV.ID,
 			PkgPath:        v0.PkgPath,
 			CurrentVersion: ci.ModuleVersions[v0.ModPath],
-			FixedVersion:   "v" + latestFixed(v0.OSV.Affected),
+			FixedVersion:   "v" + govulncheck.LatestFixed(v0.OSV.Affected),
 			Reference:      fmt.Sprintf("https://pkg.go.dev/vuln/%s", v0.OSV.ID),
 			Details:        v0.OSV.Details,
 		}
@@ -58,7 +58,7 @@
 		for _, v := range vg {
 			if css := ci.CallStacks[v]; len(css) > 0 {
 				vn.Stacks = append(vn.Stacks, callstack{
-					Summary: summarizeCallStack(css[0], ci.TopPackages, v.PkgPath),
+					Summary: govulncheck.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath),
 					Stack:   css[0],
 				})
 			}
diff --git a/cmd/govulncheck/internal/govulncheck/util.go b/cmd/govulncheck/internal/govulncheck/util.go
new file mode 100644
index 0000000..6699236
--- /dev/null
+++ b/cmd/govulncheck/internal/govulncheck/util.go
@@ -0,0 +1,106 @@
+// 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 (
+	"fmt"
+	"strings"
+
+	"golang.org/x/mod/semver"
+	"golang.org/x/vuln/osv"
+	"golang.org/x/vuln/vulncheck"
+)
+
+// LatestFixed returns the latest fixed version in the list of affected ranges,
+// or the empty string if there are no fixed versions.
+func LatestFixed(as []osv.Affected) string {
+	v := ""
+	for _, a := range as {
+		for _, r := range a.Ranges {
+			if r.Type == osv.TypeSemver {
+				for _, e := range r.Events {
+					if e.Fixed != "" && (v == "" || semver.Compare(e.Fixed, v) > 0) {
+						v = e.Fixed
+					}
+				}
+			}
+		}
+	}
+	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.
+func SummarizeCallStack(cs vulncheck.CallStack, topPkgs map[string]bool, vulnPkg string) string {
+	// Find the lowest function in the top packages.
+	iTop := lowest(cs, func(e vulncheck.StackEntry) bool {
+		return topPkgs[PkgPath(e.Function)]
+	})
+	if iTop < 0 {
+		return ""
+	}
+	// Find the highest function in the vulnerable package that is below iTop.
+	iVuln := highest(cs[iTop+1:], func(e vulncheck.StackEntry) bool {
+		return PkgPath(e.Function) == vulnPkg
+	})
+	if iVuln < 0 {
+		return ""
+	}
+	iVuln += iTop + 1 // adjust for slice in call to highest.
+	topName := FuncName(cs[iTop].Function)
+	vulnName := FuncName(cs[iVuln].Function)
+	if iVuln == iTop+1 {
+		return fmt.Sprintf("%s calls %s", topName, vulnName)
+	}
+	return fmt.Sprintf("%s calls %s, which eventually calls %s",
+		topName, FuncName(cs[iTop+1].Function), vulnName)
+}
+
+// highest returns the highest (one with the smallest index) entry in the call
+// stack for which f returns true.
+func highest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int {
+	for i := 0; i < len(cs); i++ {
+		if f(cs[i]) {
+			return i
+		}
+	}
+	return -1
+}
+
+// lowest returns the lowest (one with the largets index) entry in the call
+// stack for which f returns true.
+func lowest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int {
+	for i := len(cs) - 1; i >= 0; i-- {
+		if f(cs[i]) {
+			return i
+		}
+	}
+	return -1
+}
+
+// PkgPath returns the package path from fn.
+func PkgPath(fn *vulncheck.FuncNode) string {
+	if fn.PkgPath != "" {
+		return fn.PkgPath
+	}
+	s := strings.TrimPrefix(fn.RecvType, "*")
+	if i := strings.LastIndexByte(s, '.'); i > 0 {
+		s = s[:i]
+	}
+	return s
+}
+
+// FuncName returns the function name from fn, adjusted
+// to remove pointer annotations.
+func FuncName(fn *vulncheck.FuncNode) string {
+	return strings.TrimPrefix(fn.String(), "*")
+}
diff --git a/cmd/govulncheck/internal/govulncheck/util_test.go b/cmd/govulncheck/internal/govulncheck/util_test.go
new file mode 100644
index 0000000..67e7772
--- /dev/null
+++ b/cmd/govulncheck/internal/govulncheck/util_test.go
@@ -0,0 +1,82 @@
+// 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 (
+	"strings"
+	"testing"
+
+	"golang.org/x/vuln/vulncheck"
+)
+
+func TestPkgPath(t *testing.T) {
+	for _, test := range []struct {
+		in   vulncheck.FuncNode
+		want string
+	}{
+		{
+			vulncheck.FuncNode{PkgPath: "math", Name: "Floor"},
+			"math",
+		},
+		{
+			vulncheck.FuncNode{RecvType: "a.com/b.T", Name: "M"},
+			"a.com/b",
+		},
+		{
+			vulncheck.FuncNode{RecvType: "*a.com/b.T", Name: "M"},
+			"a.com/b",
+		},
+	} {
+		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"
+
+	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) vulncheck.CallStack {
+	var cs vulncheck.CallStack
+	for _, e := range strings.Fields(s) {
+		parts := strings.Split(e, ".")
+		cs = append(cs, vulncheck.StackEntry{
+			Function: &vulncheck.FuncNode{
+				PkgPath: parts[0],
+				Name:    parts[1],
+			},
+		})
+	}
+	return cs
+}
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 050a142..7e8a0f0 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -28,12 +28,10 @@
 	"sort"
 	"strings"
 
-	"golang.org/x/mod/semver"
 	"golang.org/x/tools/go/buildutil"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/vuln/client"
 	"golang.org/x/vuln/cmd/govulncheck/internal/govulncheck"
-	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
 
@@ -167,11 +165,11 @@
 		v0 := vg[0]
 		line("package:", v0.PkgPath)
 		line("your version:", ci.ModuleVersions[v0.ModPath])
-		line("fixed version:", "v"+latestFixed(v0.OSV.Affected))
+		line("fixed version:", "v"+govulncheck.LatestFixed(v0.OSV.Affected))
 		var summaries []string
 		for _, v := range vg {
 			if css := ci.CallStacks[v]; len(css) > 0 {
-				if sum := summarizeCallStack(css[0], ci.TopPackages, v.PkgPath); sum != "" {
+				if sum := govulncheck.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath); sum != "" {
 					summaries = append(summaries, sum)
 				}
 			}
@@ -219,95 +217,6 @@
 	return pkgs, nil
 }
 
-// latestFixed returns the latest fixed version in the list of affected ranges,
-// or the empty string if there are no fixed versions.
-func latestFixed(as []osv.Affected) string {
-	v := ""
-	for _, a := range as {
-		for _, r := range a.Ranges {
-			if r.Type == osv.TypeSemver {
-				for _, e := range r.Events {
-					if e.Fixed != "" && (v == "" || semver.Compare(e.Fixed, v) > 0) {
-						v = e.Fixed
-					}
-				}
-			}
-		}
-	}
-	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.
-func summarizeCallStack(cs vulncheck.CallStack, topPkgs map[string]bool, vulnPkg string) string {
-	// Find the lowest function in the top packages.
-	iTop := lowest(cs, func(e vulncheck.StackEntry) bool {
-		return topPkgs[pkgPath(e.Function)]
-	})
-	if iTop < 0 {
-		return ""
-	}
-	// Find the highest function in the vulnerable package that is below iTop.
-	iVuln := highest(cs[iTop+1:], func(e vulncheck.StackEntry) bool {
-		return pkgPath(e.Function) == vulnPkg
-	})
-	if iVuln < 0 {
-		return ""
-	}
-	iVuln += iTop + 1 // adjust for slice in call to highest.
-	topName := funcName(cs[iTop].Function)
-	vulnName := funcName(cs[iVuln].Function)
-	if iVuln == iTop+1 {
-		return fmt.Sprintf("%s calls %s", topName, vulnName)
-	}
-	return fmt.Sprintf("%s calls %s, which eventually calls %s",
-		topName, funcName(cs[iTop+1].Function), vulnName)
-}
-
-// highest returns the highest (one with the smallest index) entry in the call
-// stack for which f returns true.
-func highest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int {
-	for i := 0; i < len(cs); i++ {
-		if f(cs[i]) {
-			return i
-		}
-	}
-	return -1
-}
-
-// lowest returns the lowest (one with the largets index) entry in the call
-// stack for which f returns true.
-func lowest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int {
-	for i := len(cs) - 1; i >= 0; i-- {
-		if f(cs[i]) {
-			return i
-		}
-	}
-	return -1
-}
-
-func pkgPath(fn *vulncheck.FuncNode) string {
-	if fn.PkgPath != "" {
-		return fn.PkgPath
-	}
-	s := strings.TrimPrefix(fn.RecvType, "*")
-	if i := strings.LastIndexByte(s, '.'); i > 0 {
-		s = s[:i]
-	}
-	return s
-}
-
-func funcName(fn *vulncheck.FuncNode) string {
-	return strings.TrimPrefix(fn.String(), "*")
-}
-
 // compact replaces consecutive runs of equal elements with a single copy.
 // This is like the uniq command found on Unix.
 // compact modifies the contents of the slice s; it does not create a new slice.
diff --git a/cmd/govulncheck/main_test.go b/cmd/govulncheck/main_test.go
index 9f0a6d8..28f3553 100644
--- a/cmd/govulncheck/main_test.go
+++ b/cmd/govulncheck/main_test.go
@@ -15,13 +15,12 @@
 	"os/exec"
 	"path/filepath"
 	"regexp"
-	"strings"
 	"testing"
 
 	"github.com/google/go-cmdtest"
+	"golang.org/x/vuln/cmd/govulncheck/internal/govulncheck"
 	"golang.org/x/vuln/internal/buildtest"
 	"golang.org/x/vuln/osv"
-	"golang.org/x/vuln/vulncheck"
 )
 
 var update = flag.Bool("update", false, "update test files with results")
@@ -158,80 +157,10 @@
 		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			got := latestFixed(test.in)
+			got := govulncheck.LatestFixed(test.in)
 			if got != test.want {
 				t.Errorf("got %q, want %q", got, test.want)
 			}
 		})
 	}
 }
-
-func TestPkgPath(t *testing.T) {
-	for _, test := range []struct {
-		in   vulncheck.FuncNode
-		want string
-	}{
-		{
-			vulncheck.FuncNode{PkgPath: "math", Name: "Floor"},
-			"math",
-		},
-		{
-			vulncheck.FuncNode{RecvType: "a.com/b.T", Name: "M"},
-			"a.com/b",
-		},
-		{
-			vulncheck.FuncNode{RecvType: "*a.com/b.T", Name: "M"},
-			"a.com/b",
-		},
-	} {
-		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"
-
-	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) vulncheck.CallStack {
-	var cs vulncheck.CallStack
-	for _, e := range strings.Fields(s) {
-		parts := strings.Split(e, ".")
-		cs = append(cs, vulncheck.StackEntry{
-			Function: &vulncheck.FuncNode{
-				PkgPath: parts[0],
-				Name:    parts[1],
-			},
-		})
-	}
-	return cs
-}