gopls/internal/vulncheck: copy logic of govulncheck -html

reference commit: b2400d8
The latest relevant change in the code copied is CL 403075.

Change-Id: If50cb4e0096e4f33876236cf8620430e1bcfcd86
Reviewed-on: https://go-review.googlesource.com/c/tools/+/405795
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 65fa5c5..f868a48 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -296,6 +296,7 @@
 		"CurrentVersion": string,
 		"FixedVersion": string,
 		"CallStacks": [][]golang.org/x/tools/internal/lsp/command.StackEntry,
+		"CallStackSummaries": []string,
 	},
 }
 ```
diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go
index 06ddacd..3fd9d03 100644
--- a/gopls/internal/vulncheck/command.go
+++ b/gopls/internal/vulncheck/command.go
@@ -9,7 +9,6 @@
 
 import (
 	"context"
-	"fmt"
 	"log"
 	"os"
 	"strings"
@@ -68,25 +67,13 @@
 
 // Run runs the govulncheck after loading packages using the provided packages.Config.
 func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) (_ []Vuln, err error) {
-	// TODO: how&where can we ensure cfg is the right config for the given patterns?
-
-	// vulncheck.Source may panic if the packages are incomplete. (e.g. broken code or failed dependency fetch)
-	defer func() {
-		if r := recover(); r != nil {
-			err = fmt.Errorf("cannot run vulncheck: %v", r)
-		}
-	}()
-	return c.run(ctx, cfg, patterns)
-}
-
-func (c *cmd) run(ctx context.Context, packagesCfg *packages.Config, patterns []string) ([]Vuln, error) {
-	packagesCfg.Mode |= packages.NeedModule | packages.NeedName | packages.NeedFiles |
+	cfg.Mode |= packages.NeedModule | packages.NeedName | packages.NeedFiles |
 		packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes |
 		packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps
 
 	log.Println("loading packages...")
 
-	loadedPkgs, err := packages.Load(packagesCfg, patterns...)
+	loadedPkgs, err := packages.Load(cfg, patterns...)
 	if err != nil {
 		log.Printf("package load failed: %v", err)
 		return nil, err
@@ -94,53 +81,58 @@
 	log.Printf("loaded %d packages\n", len(loadedPkgs))
 
 	pkgs := vulncheck.Convert(loadedPkgs)
-	res, err := vulncheck.Source(ctx, pkgs, &vulncheck.Config{
-		Client:      c.Client,
-		ImportsOnly: false,
+	r, err := vulncheck.Source(ctx, pkgs, &vulncheck.Config{
+		Client: c.Client,
 	})
-	cs := vulncheck.CallStacks(res)
+	if err != nil {
+		return nil, err
+	}
 
-	return toVulns(loadedPkgs, cs)
+	// Skip vulns that are in the import graph but have no calls to them.
+	var vulns []*vulncheck.Vuln
+	for _, v := range r.Vulns {
+		if v.CallSink != 0 {
+			vulns = append(vulns, v)
+		}
+	}
 
+	callStacks := vulncheck.CallStacks(r)
+	// Create set of top-level packages, used to find representative symbols
+	topPackages := map[string]bool{}
+	for _, p := range pkgs {
+		topPackages[p.PkgPath] = true
+	}
+	vulnGroups := groupByIDAndPackage(vulns)
+	moduleVersions := moduleVersionMap(r.Modules)
+
+	return toVulns(callStacks, moduleVersions, topPackages, vulnGroups)
 	// TODO: add import graphs.
 }
 
-func packageModule(p *packages.Package) *packages.Module {
-	m := p.Module
-	if m == nil {
-		return nil
-	}
-	if r := m.Replace; r != nil {
-		return r
-	}
-	return m
-}
-
-func toVulns(pkgs []*packages.Package, callstacks map[*vulncheck.Vuln][]vulncheck.CallStack) ([]Vuln, error) {
-	// Build a map from module paths to versions.
-	moduleVersions := map[string]string{}
-	packages.Visit(pkgs, nil, func(p *packages.Package) {
-		if m := packageModule(p); m != nil {
-			moduleVersions[m.Path] = m.Version
-		}
-	})
-
+func toVulns(callStacks map[*vulncheck.Vuln][]vulncheck.CallStack, moduleVersions map[string]string, topPackages map[string]bool, vulnGroups [][]*vulncheck.Vuln) ([]Vuln, error) {
 	var vulns []Vuln
-	for v, trace := range callstacks {
-		if len(trace) == 0 {
-			continue
-		}
+
+	for _, vg := range vulnGroups {
+		v0 := vg[0]
 		vuln := Vuln{
-			ID:             v.OSV.ID,
-			Details:        v.OSV.Details,
-			Aliases:        v.OSV.Aliases,
-			Symbol:         v.Symbol,
-			PkgPath:        v.PkgPath,
-			ModPath:        v.ModPath,
-			URL:            href(v.OSV),
-			CurrentVersion: moduleVersions[v.ModPath],
-			FixedVersion:   fixedVersion(v.OSV),
-			CallStacks:     toCallStacks(trace),
+			ID:             v0.OSV.ID,
+			PkgPath:        v0.PkgPath,
+			CurrentVersion: moduleVersions[v0.ModPath],
+			FixedVersion:   latestFixed(v0.OSV.Affected),
+			Details:        v0.OSV.Details,
+
+			Aliases: v0.OSV.Aliases,
+			Symbol:  v0.Symbol,
+			ModPath: v0.ModPath,
+			URL:     href(v0.OSV),
+		}
+
+		// Keep first call stack for each vuln.
+		for _, v := range vg {
+			if css := callStacks[v]; len(css) > 0 {
+				vuln.CallStacks = append(vuln.CallStacks, toCallStack(css[0]))
+				vuln.CallStackSummaries = append(vuln.CallStackSummaries, summarizeCallStack(css[0], topPackages, v.PkgPath))
+			}
 		}
 		vulns = append(vulns, vuln)
 	}
diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go
index a72e2e0..f689ab9 100644
--- a/gopls/internal/vulncheck/command_test.go
+++ b/gopls/internal/vulncheck/command_test.go
@@ -25,7 +25,6 @@
 	"golang.org/x/tools/internal/lsp/tests"
 	"golang.org/x/vuln/client"
 	"golang.org/x/vuln/osv"
-	"golang.org/x/vuln/vulncheck"
 )
 
 func TestCmd_Run(t *testing.T) {
@@ -54,42 +53,31 @@
 					URL:            "https://pkg.go.dev/vuln/GO-2022-01",
 					CurrentVersion: "v1.1.3",
 					FixedVersion:   "v1.0.4",
+					CallStackSummaries: []string{
+						"golang.org/entry/x.X calls golang.org/amod/avuln.VulnData.Vuln1",
+						"golang.org/entry/x.X calls golang.org/cmod/c.C1, which eventually calls golang.org/amod/avuln.VulnData.Vuln2",
+					},
 				},
 				CallStacksStr: []string{
-					"golang.org/cmod/c.I.t0 called from golang.org/entry/x.X [approx.] (x.go:8)\n" +
+					"golang.org/entry/x.X [approx.] (x.go:8)\n" +
 						"golang.org/amod/avuln.VulnData.Vuln1 (avuln.go:3)\n",
-				},
-			},
-			{
-				Vuln: Vuln{
-					ID:             "GO-2022-01",
-					Symbol:         "VulnData.Vuln2",
-					PkgPath:        "golang.org/amod/avuln",
-					ModPath:        "golang.org/amod",
-					URL:            "https://pkg.go.dev/vuln/GO-2022-01",
-					CurrentVersion: "v1.1.3",
-					FixedVersion:   "v1.0.4",
-				},
-				CallStacksStr: []string{
-					"C1 called from golang.org/entry/x.X (x.go:8)\n" +
-						"Vuln2 called from golang.org/cmod/c.C1 (c.go:13)\n" +
+					"golang.org/entry/x.X (x.go:8)\n" +
+						"golang.org/cmod/c.C1 (c.go:13)\n" +
 						"golang.org/amod/avuln.VulnData.Vuln2 (avuln.go:4)\n",
 				},
 			},
 			{
 				Vuln: Vuln{
-					ID:             "GO-2022-02",
-					Symbol:         "Vuln",
-					PkgPath:        "golang.org/bmod/bvuln",
-					ModPath:        "golang.org/bmod",
-					URL:            "https://pkg.go.dev/vuln/GO-2022-02",
-					CurrentVersion: "v0.5.0",
+					ID:                 "GO-2022-02",
+					Symbol:             "Vuln",
+					PkgPath:            "golang.org/bmod/bvuln",
+					ModPath:            "golang.org/bmod",
+					URL:                "https://pkg.go.dev/vuln/GO-2022-02",
+					CurrentVersion:     "v0.5.0",
+					CallStackSummaries: []string{"golang.org/entry/y.Y calls golang.org/bmod/bvuln.Vuln"},
 				},
 				CallStacksStr: []string{
-					"t0 called from golang.org/entry/y.Y [approx.] (y.go:5)\n" +
-						"golang.org/bmod/bvuln.Vuln (bvuln.go:2)\n",
-					"Y called from golang.org/entry/x.CallY (x.go:12)\n" +
-						"t0 called from golang.org/entry/y.Y [approx.] (y.go:5)\n" +
+					"golang.org/entry/y.Y [approx.] (y.go:5)\n" +
 						"golang.org/bmod/bvuln.Vuln (bvuln.go:2)\n",
 				},
 			},
@@ -97,8 +85,8 @@
 		// sort reports for stability before comparison.
 		for _, rpts := range [][]report{got, want} {
 			sort.Slice(rpts, func(i, j int) bool {
-				a, b := got[i], got[j]
-				if b.ID != b.ID {
+				a, b := rpts[i], rpts[j]
+				if a.ID != b.ID {
 					return a.ID < b.ID
 				}
 				if a.PkgPath != b.PkgPath {
@@ -254,50 +242,6 @@
 	},
 }
 
-var goldenReport1 = []string{`
-{
-	ID: "GO-2022-01",
-	Symbol: "VulnData.Vuln1",
-	PkgPath: "golang.org/amod/avuln",
-	ModPath: "golang.org/amod",
-	URL: "https://pkg.go.dev/vuln/GO-2022-01",
-	CurrentVersion "v1.1.3",
-	FixedVersion "v1.0.4",
-	"call_stacks": [
-	 "golang.org/cmod/c.I.t0 called from golang.org/entry/x.X [approx.] (x.go:8)\ngolang.org/amod/avuln.VulnData.Vuln1 (avuln.go:3)\n\n"
-	]
-}
-`,
-	`
-{
-	"id": "GO-2022-02",
-	"symbol": "Vuln",
-	"pkg_path": "golang.org/bmod/bvuln",
-	"mod_path": "golang.org/bmod",
-	"url": "https://pkg.go.dev/vuln/GO-2022-02",
-	"current_version": "v0.5.0",
-	"call_stacks": [
-	 "t0 called from golang.org/entry/y.Y [approx.] (y.go:5)\ngolang.org/bmod/bvuln.Vuln (bvuln.go:2)\n\n",
-	 "Y called from golang.org/entry/x.CallY (x.go:12)\nt0 called from golang.org/entry/y.Y [approx.] (y.go:5)\ngolang.org/bmod/bvuln.Vuln (bvuln.go:2)\n\n"
-	]
-}
-`,
-	`
-{
-	"id": "GO-2022-01",
-	"symbol": "VulnData.Vuln2",
-	"pkg_path": "golang.org/amod/avuln",
-	"mod_path": "golang.org/amod",
-	"url": "https://pkg.go.dev/vuln/GO-2022-01",
-	"current_version": "v1.1.3",
-	FixedVersion: "v1.0.4",
-	"call_stacks": [
-	 "C1 called from golang.org/entry/x.X (x.go:8)\nVuln2 called from golang.org/cmod/c.C1 (c.go:13)\ngolang.org/amod/avuln.VulnData.Vuln2 (avuln.go:4)\n\n"
-	]
-}
-`,
-}
-
 type mockClient struct {
 	client.Client
 	ret map[string][]*osv.Entry
@@ -347,19 +291,6 @@
 	test(ctx, snapshot)
 }
 
-func sortStrs(s []string) []string {
-	sort.Strings(s)
-	return s
-}
-
-func pkgPaths(pkgs []*vulncheck.Package) []string {
-	var r []string
-	for _, p := range pkgs {
-		r = append(r, p.PkgPath)
-	}
-	return sortStrs(r)
-}
-
 // TODO: expose this as a method of Snapshot.
 func packagesCfg(ctx context.Context, snapshot source.Snapshot) *packages.Config {
 	view := snapshot.View()
diff --git a/gopls/internal/vulncheck/util.go b/gopls/internal/vulncheck/util.go
index a85b55b..e2a437b 100644
--- a/gopls/internal/vulncheck/util.go
+++ b/gopls/internal/vulncheck/util.go
@@ -10,47 +10,132 @@
 import (
 	"fmt"
 	"go/token"
+	"sort"
 	"strings"
 
+	"golang.org/x/mod/semver"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
 
-// fixedVersion returns the semantic version of the module
-// version with a fix. The semantic version is
-// as defined by SemVer 2.0.0, with no leading “v” prefix.
-// Returns an empty string if there is no reported fix.
-func fixedVersion(info *osv.Entry) string {
-	var fixed string
-	for _, a := range info.Affected {
+// TODO(hyangah): automate copy of golang.org/x/vuln/cmd.
+
+// 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
+}
+
+func groupByIDAndPackage(vs []*vulncheck.Vuln) [][]*vulncheck.Vuln {
+	groups := map[[2]string][]*vulncheck.Vuln{}
+	for _, v := range vs {
+		key := [2]string{v.OSV.ID, v.PkgPath}
+		groups[key] = append(groups[key], v)
+	}
+
+	var res [][]*vulncheck.Vuln
+	for _, g := range groups {
+		res = append(res, g)
+	}
+	sort.Slice(res, func(i, j int) bool {
+		return res[i][0].PkgPath < res[j][0].PkgPath
+	})
+	return res
+}
+
+// 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 != "SEMVER" {
-				continue
-			}
-			for _, e := range r.Events {
-				if e.Fixed != "" {
-					// assuming the later entry has higher semver.
-					// TODO: check assumption.
-					fixed = "v" + e.Fixed
+			if r.Type == osv.TypeSemver {
+				for _, e := range r.Events {
+					if e.Fixed != "" && (v == "" || semver.Compare(e.Fixed, v) > 0) {
+						v = e.Fixed
+					}
 				}
 			}
 		}
 	}
-	return fixed
+	if v == "" || v[0] == 'v' {
+		return v
+	}
+	return "v" + v
 }
 
-const maxNumCallStacks = 64
+// 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)
+}
 
-func toCallStacks(src []vulncheck.CallStack) []CallStack {
-	if len(src) > maxNumCallStacks {
-		src = src[:maxNumCallStacks]
+// 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
+		}
 	}
-	var dest []CallStack
-	for _, s := range src {
-		dest = append(dest, toCallStack(s))
+	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 dest
+	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 toCallStack(src vulncheck.CallStack) CallStack {
@@ -66,8 +151,7 @@
 	pos := f.Pos
 	desc := funcName(f)
 	if src.Call != nil {
-		pos = src.Call.Pos
-		desc = funcNameInCallSite(call) + " called from " + desc
+		pos = src.Call.Pos // Exact call site position is helpful.
 		if !call.Resolved {
 			// In case of a statically unresolved call site, communicate to the client
 			// that this was approximately resolved to f
@@ -86,13 +170,6 @@
 	return strings.TrimPrefix(fn.String(), "*")
 }
 
-func funcNameInCallSite(call *vulncheck.CallSite) string {
-	if call.RecvType == "" {
-		return call.Name
-	}
-	return fmt.Sprintf("%s.%s", call.RecvType, call.Name)
-}
-
 // href returns a URL embedded in the entry if any.
 // If no suitable URL is found, it returns a default entry in
 // pkg.go.dev/vuln.
diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go
index 9aecfbe..8e4b105 100644
--- a/internal/lsp/command/interface.go
+++ b/internal/lsp/command/interface.go
@@ -380,5 +380,8 @@
 	// Example call stacks.
 	CallStacks []CallStack `json:",omitempty"`
 
+	// Short description of each call stack in CallStacks.
+	CallStackSummaries []string `json:",omitempty"`
+
 	// TODO: import graph & module graph.
 }
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index c2c1f82..0695efc 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -675,7 +675,7 @@
 			Title:     "Run vulncheck (experimental)",
 			Doc:       "Run vulnerability check (`govulncheck`).",
 			ArgDoc:    "{\n\t// Dir is the directory from which vulncheck will run from.\n\t\"Dir\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}",
-			ResultDoc: "{\n\t\"Vuln\": []{\n\t\t\"ID\": string,\n\t\t\"Details\": string,\n\t\t\"Aliases\": []string,\n\t\t\"Symbol\": string,\n\t\t\"PkgPath\": string,\n\t\t\"ModPath\": string,\n\t\t\"URL\": string,\n\t\t\"CurrentVersion\": string,\n\t\t\"FixedVersion\": string,\n\t\t\"CallStacks\": [][]golang.org/x/tools/internal/lsp/command.StackEntry,\n\t},\n}",
+			ResultDoc: "{\n\t\"Vuln\": []{\n\t\t\"ID\": string,\n\t\t\"Details\": string,\n\t\t\"Aliases\": []string,\n\t\t\"Symbol\": string,\n\t\t\"PkgPath\": string,\n\t\t\"ModPath\": string,\n\t\t\"URL\": string,\n\t\t\"CurrentVersion\": string,\n\t\t\"FixedVersion\": string,\n\t\t\"CallStacks\": [][]golang.org/x/tools/internal/lsp/command.StackEntry,\n\t\t\"CallStackSummaries\": []string,\n\t},\n}",
 		},
 		{
 			Command:   "gopls.start_debugging",