internal/govulncheck: implement new Source and Binary APIs

This is a first in the series of refactoring CLs that implement new
govulncheck API. Please see opening message for more details on actual
changes.

For golang/go#56042

Change-Id: I6ec699204e21e94bf577220bfe040ed6db873d29
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/446715
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 4a567d1..9cdd971 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -25,8 +25,7 @@
 	tagsFlag    buildutil.TagsFlag
 
 	// testmode flags. See main_testmode.go.
-	dirFlag         string
-	summaryJSONFlag bool
+	dirFlag string
 )
 
 func init() {
@@ -63,10 +62,7 @@
 	outputType := govulncheck.OutputTypeText
 	if *jsonFlag {
 		outputType = govulncheck.OutputTypeJSON
-	} else if summaryJSONFlag { // accessible only in testmode.
-		outputType = govulncheck.OutputTypeSummary
-	}
-	if outputType == govulncheck.OutputTypeText && *verboseFlag {
+	} else if *verboseFlag {
 		outputType = govulncheck.OutputTypeVerbose
 	}
 
diff --git a/cmd/govulncheck/main_testmode.go b/cmd/govulncheck/main_testmode.go
index 78c00d4..0ac803b 100644
--- a/cmd/govulncheck/main_testmode.go
+++ b/cmd/govulncheck/main_testmode.go
@@ -10,5 +10,4 @@
 
 func init() {
 	flag.StringVar(&dirFlag, "dir", "", "directory to use for loading source files")
-	flag.BoolVar(&summaryJSONFlag, "summary-json", false, "output govulnchecklib.Summary JSON")
 }
diff --git a/cmd/govulncheck/testdata/json-binary.ct b/cmd/govulncheck/testdata/json-binary.ct
index dfe929f..1c9b0af 100644
--- a/cmd/govulncheck/testdata/json-binary.ct
+++ b/cmd/govulncheck/testdata/json-binary.ct
@@ -1,30 +1,10 @@
 $ govulncheck -json ${novuln_binary}
 {
-	"Calls": null,
-	"Imports": null,
-	"Requires": null,
-	"Vulns": null,
-	"Modules": [
-		{
-			"Path": "golang.org/x/text",
-			"Version": "v0.3.7",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "stdlib",
-			"Version": "",
-			"Dir": "",
-			"Replace": null
-		}
-	]
+	"Vulns": null
 }
 
 $ govulncheck -json ${vuln_binary}
 {
-	"Calls": null,
-	"Imports": null,
-	"Requires": null,
 	"Vulns": [
 		{
 			"OSV": {
@@ -87,44 +67,19 @@
 					}
 				]
 			},
-			"Symbol": "Parse",
-			"PkgPath": "golang.org/x/text/language",
-			"ModPath": "golang.org/x/text",
-			"CallSink": 0,
-			"ImportSink": 0,
-			"RequireSink": 0
-		}
-	],
-	"Modules": [
-		{
-			"Path": "github.com/tidwall/gjson",
-			"Version": "v1.9.2",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "github.com/tidwall/match",
-			"Version": "v1.1.0",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "github.com/tidwall/pretty",
-			"Version": "v1.2.0",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "golang.org/x/text",
-			"Version": "v0.3.0",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "stdlib",
-			"Version": "",
-			"Dir": "",
-			"Replace": null
+			"Modules": [
+				{
+					"Path": "golang.org/x/text",
+					"FoundVersion": "v0.3.0",
+					"FixedVersion": "v0.3.7",
+					"Packages": [
+						{
+							"Path": "golang.org/x/text/language",
+							"CallStacks": null
+						}
+					]
+				}
+			]
 		}
 	]
 }
diff --git a/cmd/govulncheck/testdata/json-summary.ct b/cmd/govulncheck/testdata/json-summary.ct
deleted file mode 100644
index 0baaccc..0000000
--- a/cmd/govulncheck/testdata/json-summary.ct
+++ /dev/null
@@ -1,268 +0,0 @@
-# Test of the -json flag.
-# TODO(zpavlinovic): add test for stdlib that works
-# on all underlying Go build systems.
-
-$ govulncheck -dir ${moddir}/novuln -summary-json .
-{
-	"Affecting": null,
-	"NonAffecting": null
-}
-
-$ govulncheck -dir ${moddir}/vuln -summary-json .
-{
-	"Affecting": [
-		{
-			"OSV": {
-				"id": "GO-2021-0113",
-				"published": "2021-10-06T17:51:21Z",
-				"modified": "2021-10-06T17:51:21Z",
-				"aliases": [
-					"CVE-2021-38561"
-				],
-				"details": "Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n",
-				"affected": [
-					{
-						"package": {
-							"name": "golang.org/x/text",
-							"ecosystem": "Go"
-						},
-						"ranges": [
-							{
-								"type": "SEMVER",
-								"events": [
-									{
-										"introduced": "0"
-									},
-									{
-										"fixed": "0.3.7"
-									}
-								]
-							}
-						],
-						"database_specific": {
-							"url": "https://pkg.go.dev/vuln/GO-2021-0113"
-						},
-						"ecosystem_specific": {
-							"imports": [
-								{
-									"path": "golang.org/x/text/language",
-									"symbols": [
-										"MatchStrings",
-										"MustParse",
-										"Parse",
-										"ParseAcceptLanguage"
-									]
-								}
-							]
-						}
-					}
-				],
-				"references": [
-					{
-						"type": "FIX",
-						"url": "https://go.dev/cl/340830"
-					},
-					{
-						"type": "FIX",
-						"url": "https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f"
-					},
-					{
-						"type": "WEB",
-						"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-38561"
-					}
-				]
-			},
-			"PkgPath": "golang.org/x/text/language",
-			"ModPath": "golang.org/x/text",
-			"FoundIn": "v0.3.0",
-			"FixedIn": "v0.3.7",
-			"Trace": [
-				{
-					"Symbol": "Parse",
-					"Desc": ".../vuln.go:12:16: golang.org/vuln.main calls golang.org/x/text/language.Parse",
-					"Stack": [
-						{
-							"FuncName": "golang.org/vuln.main",
-							"CallSite": ".../vuln.go:12:16"
-						},
-						{
-							"FuncName": "golang.org/x/text/language.Parse",
-							"CallSite": ""
-						}
-					],
-					"Seen": 1
-				}
-			]
-		}
-	],
-	"NonAffecting": [
-		{
-			"OSV": {
-				"id": "GO-2022-0592",
-				"published": "2022-08-15T18:06:07Z",
-				"modified": "2022-08-19T22:21:47Z",
-				"aliases": [
-					"CVE-2021-42248",
-					"GHSA-c9gm-7rfj-8w5h"
-				],
-				"details": "A maliciously crafted path can cause Get and other query functions to consume excessive amounts of CPU and time.",
-				"affected": [
-					{
-						"package": {
-							"name": "github.com/tidwall/gjson",
-							"ecosystem": "Go"
-						},
-						"ranges": [
-							{
-								"type": "SEMVER",
-								"events": [
-									{
-										"introduced": "0"
-									},
-									{
-										"fixed": "1.9.3"
-									}
-								]
-							}
-						],
-						"database_specific": {
-							"url": "https://pkg.go.dev/vuln/GO-2022-0592"
-						},
-						"ecosystem_specific": {
-							"imports": [
-								{
-									"path": "github.com/tidwall/gjson",
-									"symbols": [
-										"Get",
-										"GetBytes",
-										"GetMany",
-										"GetManyBytes",
-										"Result.Get",
-										"queryMatches"
-									]
-								}
-							]
-						}
-					}
-				],
-				"references": [
-					{
-						"type": "FIX",
-						"url": "https://github.com/tidwall/gjson/commit/77a57fda87dca6d0d7d4627d512a630f89a91c96"
-					},
-					{
-						"type": "WEB",
-						"url": "https://github.com/tidwall/gjson/issues/237"
-					},
-					{
-						"type": "WEB",
-						"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-42248"
-					},
-					{
-						"type": "WEB",
-						"url": "https://github.com/advisories/GHSA-c9gm-7rfj-8w5h"
-					}
-				]
-			},
-			"PkgPath": "github.com/tidwall/gjson",
-			"ModPath": "github.com/tidwall/gjson",
-			"FoundIn": "v1.9.2",
-			"FixedIn": "v1.9.3",
-			"Trace": null
-		},
-		{
-			"OSV": {
-				"id": "GO-2021-0265",
-				"published": "2022-01-14T17:30:24Z",
-				"modified": "2022-08-19T22:21:47Z",
-				"aliases": [
-					"CVE-2020-36066",
-					"CVE-2021-42836",
-					"GHSA-ppj4-34rq-v8j9",
-					"GHSA-wjm3-fq3r-5x46"
-				],
-				"details": "GJSON allowed a ReDoS (regular expression denial of service) attack.",
-				"affected": [
-					{
-						"package": {
-							"name": "github.com/tidwall/gjson",
-							"ecosystem": "Go"
-						},
-						"ranges": [
-							{
-								"type": "SEMVER",
-								"events": [
-									{
-										"introduced": "0"
-									},
-									{
-										"fixed": "1.9.3"
-									}
-								]
-							}
-						],
-						"database_specific": {
-							"url": "https://pkg.go.dev/vuln/GO-2021-0265"
-						},
-						"ecosystem_specific": {
-							"imports": [
-								{
-									"path": "github.com/tidwall/gjson",
-									"goos": [
-										"linux",
-										"windows"
-									],
-									"goarch": [
-										"amd64"
-									],
-									"symbols": [
-										"match.Match"
-									]
-								}
-							]
-						}
-					}
-				],
-				"references": [
-					{
-						"type": "FIX",
-						"url": "https://github.com/tidwall/gjson/commit/590010fdac311cc8990ef5c97448d4fec8f29944"
-					},
-					{
-						"type": "WEB",
-						"url": "https://github.com/tidwall/gjson/compare/v1.9.2...v1.9.3"
-					},
-					{
-						"type": "WEB",
-						"url": "https://github.com/tidwall/gjson/issues/236"
-					},
-					{
-						"type": "WEB",
-						"url": "https://github.com/tidwall/gjson/issues/237"
-					},
-					{
-						"type": "WEB",
-						"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-36066"
-					},
-					{
-						"type": "WEB",
-						"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-42836"
-					},
-					{
-						"type": "WEB",
-						"url": "https://github.com/advisories/GHSA-ppj4-34rq-v8j9"
-					},
-					{
-						"type": "WEB",
-						"url": "https://github.com/advisories/GHSA-wjm3-fq3r-5x46"
-					}
-				]
-			},
-			"PkgPath": "github.com/tidwall/gjson",
-			"ModPath": "github.com/tidwall/gjson",
-			"FoundIn": "v1.9.2",
-			"FixedIn": "v1.9.3",
-			"Trace": null
-		}
-	]
-}
diff --git a/cmd/govulncheck/testdata/json.ct b/cmd/govulncheck/testdata/json.ct
index e7cc2b3..c58bf67 100644
--- a/cmd/govulncheck/testdata/json.ct
+++ b/cmd/govulncheck/testdata/json.ct
@@ -4,154 +4,196 @@
 
 $ govulncheck -dir ${moddir}/novuln -json .
 {
-	"Calls": {
-		"Functions": {},
-		"Entries": null
-	},
-	"Imports": {
-		"Packages": {},
-		"Entries": null
-	},
-	"Requires": {
-		"Modules": {},
-		"Entries": null
-	},
-	"Vulns": null,
-	"Modules": [
-		{
-			"Path": "golang.org/novuln",
-			"Version": "",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "golang.org/x/text",
-			"Version": "v0.3.7",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "stdlib",
-			"Version": "",
-			"Dir": "",
-			"Replace": null
-		}
-	]
+	"Vulns": null
 }
 
 $ govulncheck -dir ${moddir}/vuln -json .
 {
-	"Calls": {
-		"Functions": {
-			"1": {
-				"ID": 1,
-				"Name": "main",
-				"RecvType": "",
-				"PkgPath": "golang.org/vuln",
-				"Pos": {
-					"Filename": ".../vuln.go",
-					"Offset": 97,
-					"Line": 10,
-					"Column": 6
-				},
-				"CallSites": null
-			},
-			"2": {
-				"ID": 2,
-				"Name": "Parse",
-				"RecvType": "",
-				"PkgPath": "golang.org/x/text/language",
-				"Pos": {
-					"Filename": ".../parse.go",
-					"Offset": 5808,
-					"Line": 228,
-					"Column": 6
-				},
-				"CallSites": [
+	"Vulns": [
+		{
+			"OSV": {
+				"id": "GO-2022-0592",
+				"published": "2022-08-15T18:06:07Z",
+				"modified": "2022-08-19T22:21:47Z",
+				"aliases": [
+					"CVE-2021-42248",
+					"GHSA-c9gm-7rfj-8w5h"
+				],
+				"details": "A maliciously crafted path can cause Get and other query functions to consume excessive amounts of CPU and time.",
+				"affected": [
 					{
-						"Parent": 1,
-						"Name": "Parse",
-						"RecvType": "",
-						"Pos": {
-							"Filename": ".../vuln.go",
-							"Offset": 143,
-							"Line": 12,
-							"Column": 16
+						"package": {
+							"name": "github.com/tidwall/gjson",
+							"ecosystem": "Go"
 						},
-						"Resolved": true
+						"ranges": [
+							{
+								"type": "SEMVER",
+								"events": [
+									{
+										"introduced": "0"
+									},
+									{
+										"fixed": "1.9.3"
+									}
+								]
+							}
+						],
+						"database_specific": {
+							"url": "https://pkg.go.dev/vuln/GO-2022-0592"
+						},
+						"ecosystem_specific": {
+							"imports": [
+								{
+									"path": "github.com/tidwall/gjson",
+									"symbols": [
+										"Get",
+										"GetBytes",
+										"GetMany",
+										"GetManyBytes",
+										"Result.Get",
+										"queryMatches"
+									]
+								}
+							]
+						}
+					}
+				],
+				"references": [
+					{
+						"type": "FIX",
+						"url": "https://github.com/tidwall/gjson/commit/77a57fda87dca6d0d7d4627d512a630f89a91c96"
+					},
+					{
+						"type": "WEB",
+						"url": "https://github.com/tidwall/gjson/issues/237"
+					},
+					{
+						"type": "WEB",
+						"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-42248"
+					},
+					{
+						"type": "WEB",
+						"url": "https://github.com/advisories/GHSA-c9gm-7rfj-8w5h"
 					}
 				]
-			}
+			},
+			"Modules": [
+				{
+					"Path": "github.com/tidwall/gjson",
+					"FoundVersion": "v1.9.2",
+					"FixedVersion": "v1.9.3",
+					"Packages": [
+						{
+							"Path": "github.com/tidwall/gjson",
+							"CallStacks": null
+						}
+					]
+				}
+			]
 		},
-		"Entries": [
-			1
-		]
-	},
-	"Imports": {
-		"Packages": {
-			"1": {
-				"ID": 1,
-				"Name": "gjson",
-				"Path": "github.com/tidwall/gjson",
-				"Module": 1,
-				"ImportedBy": [
-					3
+		{
+			"OSV": {
+				"id": "GO-2021-0265",
+				"published": "2022-01-14T17:30:24Z",
+				"modified": "2022-08-19T22:21:47Z",
+				"aliases": [
+					"CVE-2020-36066",
+					"CVE-2021-42836",
+					"GHSA-ppj4-34rq-v8j9",
+					"GHSA-wjm3-fq3r-5x46"
+				],
+				"details": "GJSON allowed a ReDoS (regular expression denial of service) attack.",
+				"affected": [
+					{
+						"package": {
+							"name": "github.com/tidwall/gjson",
+							"ecosystem": "Go"
+						},
+						"ranges": [
+							{
+								"type": "SEMVER",
+								"events": [
+									{
+										"introduced": "0"
+									},
+									{
+										"fixed": "1.9.3"
+									}
+								]
+							}
+						],
+						"database_specific": {
+							"url": "https://pkg.go.dev/vuln/GO-2021-0265"
+						},
+						"ecosystem_specific": {
+							"imports": [
+								{
+									"path": "github.com/tidwall/gjson",
+									"goos": [
+										"linux",
+										"windows"
+									],
+									"goarch": [
+										"amd64"
+									],
+									"symbols": [
+										"match.Match"
+									]
+								}
+							]
+						}
+					}
+				],
+				"references": [
+					{
+						"type": "FIX",
+						"url": "https://github.com/tidwall/gjson/commit/590010fdac311cc8990ef5c97448d4fec8f29944"
+					},
+					{
+						"type": "WEB",
+						"url": "https://github.com/tidwall/gjson/compare/v1.9.2...v1.9.3"
+					},
+					{
+						"type": "WEB",
+						"url": "https://github.com/tidwall/gjson/issues/236"
+					},
+					{
+						"type": "WEB",
+						"url": "https://github.com/tidwall/gjson/issues/237"
+					},
+					{
+						"type": "WEB",
+						"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-36066"
+					},
+					{
+						"type": "WEB",
+						"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-42836"
+					},
+					{
+						"type": "WEB",
+						"url": "https://github.com/advisories/GHSA-ppj4-34rq-v8j9"
+					},
+					{
+						"type": "WEB",
+						"url": "https://github.com/advisories/GHSA-wjm3-fq3r-5x46"
+					}
 				]
 			},
-			"2": {
-				"ID": 2,
-				"Name": "language",
-				"Path": "golang.org/x/text/language",
-				"Module": 3,
-				"ImportedBy": [
-					3
-				]
-			},
-			"3": {
-				"ID": 3,
-				"Name": "main",
-				"Path": "golang.org/vuln",
-				"Module": 2,
-				"ImportedBy": null
-			}
+			"Modules": [
+				{
+					"Path": "github.com/tidwall/gjson",
+					"FoundVersion": "v1.9.2",
+					"FixedVersion": "v1.9.3",
+					"Packages": [
+						{
+							"Path": "github.com/tidwall/gjson",
+							"CallStacks": null
+						}
+					]
+				}
+			]
 		},
-		"Entries": [
-			3
-		]
-	},
-	"Requires": {
-		"Modules": {
-			"1": {
-				"ID": 1,
-				"Path": "github.com/tidwall/gjson",
-				"Version": "v1.9.2",
-				"Replace": 0,
-				"RequiredBy": [
-					2
-				]
-			},
-			"2": {
-				"ID": 2,
-				"Path": "golang.org/vuln",
-				"Version": "",
-				"Replace": 0,
-				"RequiredBy": null
-			},
-			"3": {
-				"ID": 3,
-				"Path": "golang.org/x/text",
-				"Version": "v0.3.0",
-				"Replace": 0,
-				"RequiredBy": [
-					2
-				]
-			}
-		},
-		"Entries": [
-			2
-		]
-	},
-	"Vulns": [
 		{
 			"OSV": {
 				"id": "GO-2021-0113",
@@ -213,50 +255,48 @@
 					}
 				]
 			},
-			"Symbol": "Parse",
-			"PkgPath": "golang.org/x/text/language",
-			"ModPath": "golang.org/x/text",
-			"CallSink": 2,
-			"ImportSink": 2,
-			"RequireSink": 3
-		}
-	],
-	"Modules": [
-		{
-			"Path": "github.com/tidwall/gjson",
-			"Version": "v1.9.2",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "github.com/tidwall/match",
-			"Version": "v1.1.0",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "github.com/tidwall/pretty",
-			"Version": "v1.2.0",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "golang.org/vuln",
-			"Version": "",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "golang.org/x/text",
-			"Version": "v0.3.0",
-			"Dir": "",
-			"Replace": null
-		},
-		{
-			"Path": "stdlib",
-			"Version": "",
-			"Dir": "",
-			"Replace": null
+			"Modules": [
+				{
+					"Path": "golang.org/x/text",
+					"FoundVersion": "v0.3.0",
+					"FixedVersion": "v0.3.7",
+					"Packages": [
+						{
+							"Path": "golang.org/x/text/language",
+							"CallStacks": [
+								{
+									"Symbol": "Parse",
+									"Summary": ".../vuln.go:12:16: golang.org/vuln.main calls golang.org/x/text/language.Parse",
+									"Frames": [
+										{
+											"PkgPath": "golang.org/vuln",
+											"FuncName": "main",
+											"RecvType": "",
+											"Position": {
+												"Filename": ".../vuln.go",
+												"Offset": 143,
+												"Line": 12,
+												"Column": 16
+											}
+										},
+										{
+											"PkgPath": "golang.org/x/text/language",
+											"FuncName": "Parse",
+											"RecvType": "",
+											"Position": {
+												"Filename": "",
+												"Offset": 0,
+												"Line": 0,
+												"Column": 0
+											}
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			]
 		}
 	]
 }
diff --git a/cmd/govulncheck/testdata/usage.ct b/cmd/govulncheck/testdata/usage.ct
index 58abd14..72afbcc 100644
--- a/cmd/govulncheck/testdata/usage.ct
+++ b/cmd/govulncheck/testdata/usage.ct
@@ -7,8 +7,6 @@
     	directory to use for loading source files
   -json
     	output JSON
-  -summary-json
-    	output govulnchecklib.Summary JSON
   -tags list
     	comma-separated list of build tags
   -test
@@ -28,8 +26,6 @@
     	directory to use for loading source files
   -json
     	output JSON
-  -summary-json
-    	output govulnchecklib.Summary JSON
   -tags list
     	comma-separated list of build tags
   -test
diff --git a/internal/govulncheck/config.go b/internal/govulncheck/config.go
index 424f205..33ac937 100644
--- a/internal/govulncheck/config.go
+++ b/internal/govulncheck/config.go
@@ -24,12 +24,6 @@
 	// OutputTypeJSON is the output type for `govulncheck -json`, which will print
 	// the JSON-encoded vulncheck.Result.
 	OutputTypeJSON = "json"
-
-	// OutputTypeSummary is the output type for `govulncheck -summary-json`, which
-	// will print the JSON-encoded govulncheck.Summary.
-	//
-	// This is only meant by use for experimental with gopls.
-	OutputTypeSummary = "summary"
 )
 
 const (
diff --git a/internal/govulncheck/inits.go b/internal/govulncheck/inits.go
new file mode 100644
index 0000000..ef18b05
--- /dev/null
+++ b/internal/govulncheck/inits.go
@@ -0,0 +1,137 @@
+// 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"
+	"go/ast"
+	"go/token"
+	"strconv"
+	"strings"
+
+	"golang.org/x/vuln/vulncheck"
+)
+
+// updateInitPositions populates non-existing positions of init functions
+// and their respective calls in callStacks (see #51575).
+func updateInitPositions(callStacks map[*vulncheck.Vuln][]vulncheck.CallStack, pkgs []*vulncheck.Package) {
+	pMap := pkgMap(pkgs)
+	for _, css := range callStacks {
+		for _, cs := range css {
+			for i, _ := range cs {
+				updateInitPosition(&cs[i], pMap)
+				if i != len(cs)-1 {
+					updateInitCallPosition(&cs[i], cs[i+1], pMap)
+				}
+			}
+		}
+	}
+}
+
+// updateInitCallPosition updates the position of a call to init in a stack frame, if
+// one already does not exist:
+//
+//	P1.init -> P2.init: position of call to P2.init is the position of "import P2"
+//	statement in P1
+//
+//	P.init -> P.init#d: P.init is an implicit init. We say it calls the explicit
+//	P.init#d at the place of "package P" statement.
+func updateInitCallPosition(curr *vulncheck.StackEntry, next vulncheck.StackEntry, pkgs map[string]*vulncheck.Package) {
+	call := curr.Call
+	if !isInit(next.Function) || (call.Pos != nil && call.Pos.IsValid()) {
+		// Skip non-init functions and inits whose call site position is available.
+		return
+	}
+
+	pkg := pkgs[curr.Function.PkgPath]
+	var pos token.Position
+	if curr.Function.Name == "init" && curr.Function.PkgPath == next.Function.PkgPath {
+		// We have implicit P.init calling P.init#d. Set the call position to
+		// be at "package P" statement position.
+		pos = packageStatementPos(pkg)
+	} else {
+		// Choose the beginning of the import statement as the position.
+		pos = importStatementPos(pkg, next.Function.PkgPath)
+	}
+
+	call.Pos = &pos
+}
+
+func importStatementPos(pkg *vulncheck.Package, importPath string) token.Position {
+	var importSpec *ast.ImportSpec
+spec:
+	for _, f := range pkg.Syntax {
+		for _, impSpec := range f.Imports {
+			// Import spec paths have quotation marks.
+			impSpecPath, err := strconv.Unquote(impSpec.Path.Value)
+			if err != nil {
+				panic(fmt.Sprintf("import specification: package path has no quotation marks: %v", err))
+			}
+			if impSpecPath == importPath {
+				importSpec = impSpec
+				break spec
+			}
+		}
+	}
+
+	if importSpec == nil {
+		// for sanity, in case of a wild call graph imprecision
+		return token.Position{}
+	}
+
+	// Choose the beginning of the import statement as the position.
+	return pkg.Fset.Position(importSpec.Pos())
+}
+
+func packageStatementPos(pkg *vulncheck.Package) token.Position {
+	if len(pkg.Syntax) == 0 {
+		return token.Position{}
+	}
+	// Choose beginning of the package statement as the position. Pick
+	// the first file since it is as good as any.
+	return pkg.Fset.Position(pkg.Syntax[0].Package)
+}
+
+// updateInitPosition updates the position of P.init function in a stack frame if one
+// is not available. The new position is the position of the "package P" statement.
+func updateInitPosition(se *vulncheck.StackEntry, pkgs map[string]*vulncheck.Package) {
+	fun := se.Function
+	if !isInit(fun) || (fun.Pos != nil && fun.Pos.IsValid()) {
+		// Skip non-init functions and inits whose position is available.
+		return
+	}
+
+	pos := packageStatementPos(pkgs[fun.PkgPath])
+	fun.Pos = &pos
+}
+
+func isInit(f *vulncheck.FuncNode) bool {
+	// A source init function, or anonymous functions used in inits, will
+	// be named "init#x" by vulncheck (more precisely, ssa), where x is a
+	// positive integer. Implicit inits are named simply "init".
+	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
+
+		for _, i := range p.Imports {
+			visit(i)
+		}
+	}
+
+	for _, p := range pkgs {
+		visit(p)
+	}
+	return m
+}
diff --git a/internal/govulncheck/source_test.go b/internal/govulncheck/inits_test.go
similarity index 100%
rename from internal/govulncheck/source_test.go
rename to internal/govulncheck/inits_test.go
diff --git a/internal/govulncheck/legacy_run.go b/internal/govulncheck/legacy_run.go
index d291140..b69bb68 100644
--- a/internal/govulncheck/legacy_run.go
+++ b/internal/govulncheck/legacy_run.go
@@ -6,22 +6,21 @@
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
 	"path/filepath"
-	"sort"
 	"strings"
 
-	"golang.org/x/exp/maps"
+	"golang.org/x/tools/go/packages"
 	"golang.org/x/vuln/client"
-	"golang.org/x/vuln/internal"
-	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
 
 // LegacyRun is the main function for the govulncheck command line tool.
-func LegacyRun(ctx context.Context, cfg LegacyConfig) (*Result, error) {
+//
+// 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, ",")
@@ -32,36 +31,30 @@
 	if err != nil {
 		return nil, err
 	}
-	vcfg := &vulncheck.Config{Client: dbClient, SourceGoVersion: cfg.GoVersion}
 
-	format := cfg.OutputType
+	format := lcfg.OutputType
 	if format == OutputTypeText || format == OutputTypeVerbose {
 		fmt.Println(introMessage)
 	}
-	var (
-		r          *vulncheck.Result
-		pkgs       []*vulncheck.Package
-		unaffected []*vulncheck.Vuln
-	)
-	switch cfg.AnalysisType {
+
+	cfg := &Config{Client: dbClient, GoVersion: lcfg.GoVersion}
+	var res *Result
+	switch lcfg.AnalysisType {
 	case AnalysisTypeBinary:
-		f, err := os.Open(cfg.Patterns[0])
+		f, err := os.Open(lcfg.Patterns[0])
 		if err != nil {
 			return nil, err
 		}
 		defer f.Close()
-		r, err = binary(ctx, f, vcfg)
-		if err != nil {
-			return nil, err
-		}
+		res, err = Binary(ctx, cfg, f)
 	case AnalysisTypeSource:
-		pkgs, err = loadPackages(cfg)
+		pkgs, err := loadPackages(lcfg)
 		if err != nil {
 			// Try to provide a meaningful and actionable error message.
-			if !fileExists(filepath.Join(cfg.SourceLoadConfig.Dir, "go.mod")) {
+			if !fileExists(filepath.Join(lcfg.SourceLoadConfig.Dir, "go.mod")) {
 				return nil, ErrNoGoMod
 			}
-			if !fileExists(filepath.Join(cfg.SourceLoadConfig.Dir, "go.sum")) {
+			if !fileExists(filepath.Join(lcfg.SourceLoadConfig.Dir, "go.sum")) {
 				return nil, ErrNoGoSum
 			}
 			if isGoVersionMismatchError(err) {
@@ -69,322 +62,76 @@
 			}
 			return nil, err
 		}
-		// If we are in GOPATH mode, then no version information will be available.
-		if inGoPathMode(pkgs) {
-			return nil, ErrNoModVersion
-		}
-
-		// Sort pkgs so that the PkgNodes returned by vulncheck.Source will be
-		// deterministic.
-		sortPackages(pkgs)
-		r, err = vulncheck.Source(ctx, pkgs, vcfg)
-		if err != nil {
-			return nil, err
-		}
-		// TODO(https://go.dev/issue/56042): add affected and unaffected logic
-		// to Result.
-		unaffected = filterUnaffected(r)
-		r.Vulns = filterCalled(r)
+		res, err = Source(ctx, cfg, pkgs)
 	default:
-		return nil, fmt.Errorf("%w: %s", ErrInvalidAnalysisType, cfg.AnalysisType)
+		return nil, fmt.Errorf("%w: %s", ErrInvalidAnalysisType, lcfg.AnalysisType)
+	}
+	if err != nil {
+		return nil, err
 	}
 
-	switch cfg.OutputType {
+	switch lcfg.OutputType {
 	case OutputTypeJSON:
 		// Following golang.org/x/tools/go/analysis/singlechecker,
 		// return 0 exit code in -json mode.
-
-		// TODO(https://go.dev/issue/56042): change output from
-		// vulncheck.Result to govulncheck.Result.
-		if err := writeJSON(r); err != nil {
+		if err := printJSON(res); err != nil {
 			return nil, err
 		}
-		return &Result{}, nil
-	case OutputTypeSummary:
-		// TODO(https://go.dev/issue/56042): delete this mode and change -json
-		// to output govulncheck.Result
-		ci := getCallInfo(r, pkgs)
-		if err := writeJSON(summary(ci, unaffected)); err != nil {
-			return nil, err
-		}
-		return &Result{}, nil
+		return res, nil
 	case OutputTypeText, OutputTypeVerbose:
-		// set of top-level packages, used to find representative symbols
-
-		// TODO(https://go.dev/issue/56042): add callinfo to govulncheck.Result
-		// See comments from http://go.dev/cl/437856.
-		ci := getCallInfo(r, pkgs)
-
-		// TODO(https://go.dev/issue/56042): move fields from output to Result
-		// struct and delete writeText.
-		writeText(r, ci, unaffected, cfg.OutputType == OutputTypeVerbose)
-	default:
-		return nil, fmt.Errorf("%w: %s", ErrInvalidOutputType, cfg.OutputType)
-	}
-	if len(r.Vulns) > 0 {
-		return nil, ErrContainsVulnerabilties
-	}
-	return &Result{}, nil
-}
-
-func writeJSON(r any) error {
-	b, err := json.MarshalIndent(r, "", "\t")
-	if err != nil {
-		return err
-	}
-	os.Stdout.Write(b)
-	fmt.Println()
-	return nil
-}
-
-const (
-	labelWidth = 16
-	lineLength = 55
-)
-
-func writeText(r *vulncheck.Result, ci *callInfo, unaffected []*vulncheck.Vuln, verbose bool) {
-	// TODO(https://go.dev/issue/56042): add uniqueVulns to govulncheck.Result.
-	uniqueVulns := map[string]bool{}
-	for _, v := range r.Vulns {
-		uniqueVulns[v.OSV.ID] = true
-	}
-	switch len(uniqueVulns) {
-	case 0:
-		fmt.Println("No vulnerabilities found.")
-	case 1:
-		fmt.Println("Found 1 known vulnerability.")
-	default:
-		fmt.Printf("Found %d known vulnerabilities.\n", len(uniqueVulns))
-	}
-	for idx, vg := range ci.vulnGroups {
-		fmt.Println()
-		// All the vulns in vg have the same PkgPath, ModPath and OSV.
-		// All have a non-zero CallSink when not in binary mode, otherwise
-		// they all have a zero CallSink.
-
-		// TODO(https://go.dev/issue/56042): add ID, details, found and fixed
-		// below to govulncheck.Result.
-		v0 := vg[0]
-		id := v0.OSV.ID
-		details := wrap(v0.OSV.Details, 80-labelWidth)
-		found := packageVersionString(v0.PkgPath, foundVersion(v0.ModPath, ci))
-		fixed := packageVersionString(v0.PkgPath, fixedVersion(v0.ModPath, v0.OSV.Affected))
-
-		var stacksBuilder strings.Builder
-		if r.Calls != nil { // there are no call stacks in binary mode
-			// TODO(https://go.dev/issue/56042): add stacks to govulncheck.Result.
-			var stacks string
-			if !verbose {
-				stacks = defaultCallStacks(vg, ci, r)
-			} else {
-				stacks = verboseCallStacks(vg, ci, r)
-			}
-			if len(stacks) > 0 {
-				stacksBuilder.WriteString(indent("\n\nCall stacks in your code:\n", 2))
-				stacksBuilder.WriteString(indent(stacks, 6))
-			}
-		}
-		// TODO(https://go.dev/issue/56042): add platform and callstack summary
-		// to govulncheck.Result
-		writeVulnerability(idx+1, id, details, stacksBuilder.String(), found, fixed, platforms(v0.OSV))
-	}
-	if len(unaffected) > 0 {
-		fmt.Println()
-		fmt.Println(informationalMessage)
-		for idx, vuln := range unaffected {
-			found := packageVersionString(vuln.PkgPath, foundVersion(vuln.ModPath, ci))
-			fixed := packageVersionString(vuln.PkgPath, fixedVersion(vuln.ModPath, vuln.OSV.Affected))
-			fmt.Println()
-			writeVulnerability(idx+1, vuln.OSV.ID, vuln.OSV.Details, "", found, fixed, platforms(vuln.OSV))
-		}
-	}
-}
-
-func writeVulnerability(idx int, id, details, callstack, found, fixed, platforms string) {
-	if fixed == "" {
-		fixed = "N/A"
-	}
-	if platforms != "" {
-		platforms = "  Platforms: " + platforms + "\n"
-	}
-	fmt.Printf(`Vulnerability #%d: %s
-%s%s
-  Found in: %s
-  Fixed in: %s
-%s  More info: https://pkg.go.dev/vuln/%s
-`, idx, id, indent(details, 2), callstack, found, fixed, platforms, id)
-}
-
-func foundVersion(modulePath string, ci *callInfo) string {
-	var found string
-	if v := ci.moduleVersions[modulePath]; v != "" {
-		found = versionString(modulePath, v[1:])
-	}
-	return found
-}
-
-func fixedVersion(modulePath string, affected []osv.Affected) string {
-	fixed := LatestFixed(affected)
-	if fixed != "" {
-		fixed = versionString(modulePath, fixed)
-	}
-	return fixed
-}
-
-func defaultCallStacks(vg []*vulncheck.Vuln, ci *callInfo, r *vulncheck.Result) string {
-	var summaries []string
-	forUniqueCallStacks(vg, ci, r, func(v *vulncheck.Vuln, cs vulncheck.CallStack, ci *callInfo) {
-		if sum := SummarizeCallStack(cs, ci.topPackages, v.PkgPath); sum != "" {
-			summaries = append(summaries, strings.TrimSpace(sum))
-		}
-	})
-
-	// Sort call stack summaries and get rid of duplicates.
-	// Note that different call stacks can yield same summaries.
-	if len(summaries) > 0 {
-		sort.Strings(summaries)
-		summaries = compact(summaries)
-	}
-	var b strings.Builder
-	for _, s := range summaries {
-		b.WriteString(s)
-		b.WriteString("\n")
-	}
-	return b.String()
-}
-
-func verboseCallStacks(vg []*vulncheck.Vuln, ci *callInfo, r *vulncheck.Result) string {
-	// Display one full call stack for each vuln.
-	i := 1
-	nMore := 0
-	var b strings.Builder
-	forUniqueCallStacks(vg, ci, r, func(v *vulncheck.Vuln, cs vulncheck.CallStack, ci *callInfo) {
-		b.WriteString(fmt.Sprintf("#%d: for function %s\n", i, v.Symbol))
-		for _, e := range cs {
-			b.WriteString(fmt.Sprintf("  %s\n", FuncName(e.Function)))
-			if pos := AbsRelShorter(FuncPos(e.Call)); pos != "" {
-				b.WriteString(fmt.Sprintf("      %s\n", pos))
-			}
-		}
-		i++
-		nMore += len(ci.callStacks[v]) - 1
-	})
-	if nMore > 0 {
-		b.WriteString(fmt.Sprintf("    There are %d more call stacks available.\n", nMore))
-		b.WriteString(fmt.Sprintf("To see all of them, pass the -json flags.\n"))
-	}
-	return b.String()
-}
-
-// forUniqueCallStacks applies f to each unique call stack of vg.
-func forUniqueCallStacks(vg []*vulncheck.Vuln, ci *callInfo, r *vulncheck.Result, f func(v *vulncheck.Vuln, cs vulncheck.CallStack, ci *callInfo)) {
-	vulnFuncs := make(map[*vulncheck.FuncNode]bool)
-	for _, v := range vg {
-		vulnFuncs[r.Calls.Functions[v.CallSink]] = true
-	}
-	for _, v := range vg {
-		vFunc := r.Calls.Functions[v.CallSink]
-		if cs := uniqueCallStack(vFunc, ci.callStacks[v], vulnFuncs); cs != nil {
-			f(v, cs, ci)
-		}
-	}
-}
-
-// uniqueCallStack returns the first member of stacks for vulnFunc that does not
-// go through skip list (except vulnFunc). Returns nil if no such stack can be found.
-func uniqueCallStack(vulnFunc *vulncheck.FuncNode, stacks []vulncheck.CallStack, skip map[*vulncheck.FuncNode]bool) vulncheck.CallStack {
-callstack:
-	for _, cs := range stacks {
-		for _, e := range cs {
-			if e.Function != vulnFunc && skip[e.Function] {
-				continue callstack
-			}
-		}
-		return cs
-	}
-	return nil
-}
-
-// platforms returns a string describing the GOOS/GOARCH pairs that the vuln affects.
-// If it affects all of them, it returns the empty string.
-func platforms(e *osv.Entry) string {
-	platforms := map[string]bool{}
-	for _, a := range e.Affected {
-		for _, p := range a.EcosystemSpecific.Imports {
-			for _, os := range p.GOOS {
-				for _, arch := range p.GOARCH {
-					platforms[os+"/"+arch] = true
+		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)
 	}
-	keys := maps.Keys(platforms)
-	sort.Strings(keys)
-	return strings.Join(keys, ", ")
 }
 
-func isFile(path string) bool {
-	s, err := os.Stat(path)
+// 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 false
+		return nil, err
 	}
-	return !s.IsDir()
-}
-
-// 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.
-//
-// Modified (generics removed) from exp/slices/slices.go.
-func compact(s []string) []string {
-	if len(s) == 0 {
-		return s
+	var perrs []packages.Error
+	packages.Visit(pkgs, nil, func(p *packages.Package) {
+		perrs = append(perrs, p.Errors...)
+	})
+	if len(perrs) > 0 {
+		err = &PackageError{perrs}
 	}
-	i := 1
-	last := s[0]
-	for _, v := range s[1:] {
-		if v != last {
-			s[i] = v
-			i++
-			last = v
-		}
-	}
-	return s[:i]
-}
-
-func packageVersionString(packagePath, version string) string {
-	if version == "" {
-		return ""
-	}
-	return fmt.Sprintf("%s@%s", packagePath, version)
-}
-
-// versionString prepends a version string prefix (`v` or `go`
-// depending on the modulePath) to the given semver-style version string.
-func versionString(modulePath, version string) string {
-	if version == "" {
-		return ""
-	}
-	v := "v" + version
-	if modulePath == internal.GoStdModulePath || modulePath == internal.GoCmdModulePath {
-		return semverToGoTag(v)
-	}
-	return v
-}
-
-// indent returns the output of prefixing n spaces to s at every line break,
-// except for empty lines. See TestIndent for examples.
-func indent(s string, n int) string {
-	b := []byte(s)
-	var result []byte
-	shouldAppend := true
-	prefix := strings.Repeat(" ", n)
-	for _, c := range b {
-		if shouldAppend && c != '\n' {
-			result = append(result, prefix...)
-		}
-		result = append(result, c)
-		shouldAppend = c == '\n'
-	}
-	return string(result)
+	return vpkgs, err
 }
diff --git a/internal/govulncheck/print.go b/internal/govulncheck/print.go
new file mode 100644
index 0000000..f503661
--- /dev/null
+++ b/internal/govulncheck/print.go
@@ -0,0 +1,218 @@
+// 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 (
+	"encoding/json"
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+
+	"golang.org/x/exp/maps"
+	"golang.org/x/vuln/osv"
+)
+
+func printJSON(r *Result) error {
+	b, err := json.MarshalIndent(r, "", "\t")
+	if err != nil {
+		return err
+	}
+	os.Stdout.Write(b)
+	fmt.Println()
+	return nil
+}
+
+const (
+	labelWidth = 16
+	lineLength = 55
+)
+
+func printText(r *Result, verbose, source bool) {
+	// unaffected are (imported) OSVs none of
+	// which vulnerabilities are called.
+	var unaffected []*Vuln
+	uniqueVulns := 0
+	for _, v := range r.Vulns {
+		if !source || v.IsCalled() {
+			uniqueVulns++
+		} else {
+			// save arbitrary Vuln for informational message
+			unaffected = append(unaffected, v)
+		}
+	}
+	switch uniqueVulns {
+	case 0:
+		fmt.Println("No vulnerabilities found.")
+	case 1:
+		fmt.Println("Found 1 known vulnerability.")
+	default:
+		fmt.Printf("Found %d known vulnerabilities.\n", uniqueVulns)
+	}
+
+	idx := 0
+	for _, v := range r.Vulns {
+		for _, m := range v.Modules {
+			for _, p := range m.Packages {
+				// In Binary mode there are no call stacks.
+				if source && len(p.CallStacks) == 0 {
+					continue
+				}
+				fmt.Println()
+
+				id := v.OSV.ID
+				details := wrap(v.OSV.Details, 80-labelWidth)
+				found := packageVersionString(p.Path, m.FoundVersion)
+				fixed := packageVersionString(p.Path, m.FixedVersion)
+
+				var stacksBuilder strings.Builder
+				if source { // there are no call stacks in binary mode
+					var stacks string
+					if !verbose {
+						stacks = defaultCallStacks(p.CallStacks)
+					} else {
+						stacks = verboseCallStacks(p.CallStacks)
+					}
+					if len(stacks) > 0 {
+						stacksBuilder.WriteString(indent("\n\nCall stacks in your code:\n", 2))
+						stacksBuilder.WriteString(indent(stacks, 6))
+					}
+				}
+				printVulnerability(idx+1, id, details, stacksBuilder.String(), found, fixed, platforms(v.OSV))
+				idx++
+			}
+		}
+	}
+	if len(unaffected) > 0 {
+		fmt.Println()
+		fmt.Println(informationalMessage)
+		idx = 0
+		for idx, un := range unaffected {
+			// We pick random module and package info for
+			// unaffected OSVs.
+			m := un.Modules[0]
+			p := m.Packages[0]
+			found := packageVersionString(p.Path, m.FoundVersion)
+			fixed := packageVersionString(p.Path, m.FixedVersion)
+			fmt.Println()
+			printVulnerability(idx+1, un.OSV.ID, un.OSV.Details, "", found, fixed, platforms(un.OSV))
+		}
+	}
+}
+
+func printVulnerability(idx int, id, details, callstack, found, fixed, platforms string) {
+	if fixed == "" {
+		fixed = "N/A"
+	}
+	if platforms != "" {
+		platforms = "  Platforms: " + platforms + "\n"
+	}
+	fmt.Printf(`Vulnerability #%d: %s
+%s%s
+  Found in: %s
+  Fixed in: %s
+%s  More info: https://pkg.go.dev/vuln/%s
+`, idx, id, indent(details, 2), callstack, found, fixed, platforms, id)
+}
+
+func defaultCallStacks(css []CallStack) string {
+	var summaries []string
+	for _, cs := range css {
+		summaries = append(summaries, cs.Summary)
+	}
+
+	// Sort call stack summaries and get rid of duplicates.
+	// Note that different call stacks can yield same summaries.
+	if len(summaries) > 0 {
+		sort.Strings(summaries)
+		summaries = compact(summaries)
+	}
+	var b strings.Builder
+	for _, s := range summaries {
+		b.WriteString(s)
+		b.WriteString("\n")
+	}
+	return b.String()
+}
+
+func verboseCallStacks(css []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", pos))
+			}
+		}
+		i++
+	}
+	return b.String()
+}
+
+// platforms returns a string describing the GOOS/GOARCH pairs that the vuln affects.
+// If it affects all of them, it returns the empty string.
+func platforms(e *osv.Entry) string {
+	platforms := map[string]bool{}
+	for _, a := range e.Affected {
+		for _, p := range a.EcosystemSpecific.Imports {
+			for _, os := range p.GOOS {
+				for _, arch := range p.GOARCH {
+					platforms[os+"/"+arch] = true
+				}
+			}
+		}
+	}
+	keys := maps.Keys(platforms)
+	sort.Strings(keys)
+	return strings.Join(keys, ", ")
+}
+
+// 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.
+//
+// Modified (generics removed) from exp/slices/slices.go.
+func compact(s []string) []string {
+	if len(s) == 0 {
+		return s
+	}
+	i := 1
+	last := s[0]
+	for _, v := range s[1:] {
+		if v != last {
+			s[i] = v
+			i++
+			last = v
+		}
+	}
+	return s[:i]
+}
+
+func packageVersionString(packagePath, version string) string {
+	if version == "" {
+		return ""
+	}
+	return fmt.Sprintf("%s@%s", packagePath, version)
+}
+
+// indent returns the output of prefixing n spaces to s at every line break,
+// except for empty lines. See TestIndent for examples.
+func indent(s string, n int) string {
+	b := []byte(s)
+	var result []byte
+	shouldAppend := true
+	prefix := strings.Repeat(" ", n)
+	for _, c := range b {
+		if shouldAppend && c != '\n' {
+			result = append(result, prefix...)
+		}
+		result = append(result, c)
+		shouldAppend = c == '\n'
+	}
+	return string(result)
+}
diff --git a/internal/govulncheck/result.go b/internal/govulncheck/result.go
index f6916f7..9e5a991 100644
--- a/internal/govulncheck/result.go
+++ b/internal/govulncheck/result.go
@@ -61,8 +61,15 @@
 // IsCalled reports whether the vulnerability is called, therefore
 // affecting the target source code or binary.
 //
-// TODO(https://go.dev/issue/56042): implement
+// TODO: add unit tests
 func (v *Vuln) IsCalled() bool {
+	for _, m := range v.Modules {
+		for _, p := range m.Packages {
+			if len(p.CallStacks) > 0 {
+				return true
+			}
+		}
+	}
 	return false
 }
 
@@ -139,12 +146,12 @@
 	// FuncName is the function name.
 	FuncName string
 
-	// RecvName is the receiver name, if the symbol is a
-	// method.
+	// RecvType is the fully qualified receiver type,
+	// if the called symbol is a method.
 	//
 	// The client can create the final symbol name by
-	// prepending RecvName to FuncName.
-	RecvName string
+	// prepending RecvType to FuncName.
+	RecvType string
 
 	// Position describes an arbitrary source position
 	// including the file, line, and column location.
diff --git a/internal/govulncheck/run.go b/internal/govulncheck/run.go
index 927f770..2bc98c5 100644
--- a/internal/govulncheck/run.go
+++ b/internal/govulncheck/run.go
@@ -6,9 +6,12 @@
 
 import (
 	"context"
-	"errors"
+	"fmt"
+	"go/token"
 	"io"
+	"sort"
 
+	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
 
@@ -20,17 +23,265 @@
 //
 // This function is used for source code analysis by cmd/govulncheck and
 // exp/govulncheck.
-//
-// TODO(https://go.dev/issue/56042): implement
 func Source(ctx context.Context, cfg *Config, pkgs []*vulncheck.Package) (*Result, error) {
-	return nil, errors.New("not implemented")
+	vcfg := &vulncheck.Config{
+		Client:          cfg.Client,
+		SourceGoVersion: cfg.GoVersion,
+	}
+	vr, err := vulncheck.Source(ctx, pkgs, vcfg)
+	if err != nil {
+		return nil, err
+	}
+	return createSourceResult(vr, pkgs), nil
 }
 
 // Binary detects presence of vulnerable symbols in exe.
 //
 // This function is used for binary analysis by cmd/govulncheck.
-//
-// TODO(https://go.dev/issue/56042): implement
 func Binary(ctx context.Context, cfg *Config, exe io.ReaderAt) (*Result, error) {
-	return nil, errors.New("not implemented")
+	vcfg := &vulncheck.Config{
+		Client: cfg.Client,
+	}
+	vr, err := binary(ctx, exe, vcfg)
+	if err != nil {
+		return nil, err
+	}
+	return createBinaryResult(vr), nil
+}
+
+func createSourceResult(vr *vulncheck.Result, pkgs []*vulncheck.Package) *Result {
+	topPkgs := map[string]bool{}
+	for _, p := range pkgs {
+		topPkgs[p.PkgPath] = true
+	}
+	modVersions := moduleVersionMap(vr.Modules)
+	callStacks := vulncheck.CallStacks(vr)
+
+	type key struct {
+		id  string
+		pkg string
+		mod string
+	}
+	// Collect all called symbols for a package.
+	// Needed for creating unique call stacks.
+	vulnsPerPkg := make(map[key][]*vulncheck.Vuln)
+	for _, vv := range vr.Vulns {
+		if vv.CallSink != 0 {
+			k := key{id: vv.OSV.ID, pkg: vv.PkgPath, mod: vv.ModPath}
+			vulnsPerPkg[k] = append(vulnsPerPkg[k], vv)
+		}
+	}
+
+	// Create Result where each vulncheck.Vuln{OSV, ModPath, PkgPath} becomes
+	// a separate Vuln{OSV, Modules{Packages{PkgPath}}} entry. We merge the
+	// results later.
+	r := &Result{}
+	for _, vv := range vr.Vulns {
+		p := &Package{Path: vv.PkgPath}
+		m := &Module{
+			Path:         vv.ModPath,
+			FoundVersion: foundVersion(vv.ModPath, modVersions),
+			FixedVersion: fixedVersion(vv.ModPath, vv.OSV.Affected),
+			Packages:     []*Package{p},
+		}
+		v := &Vuln{OSV: vv.OSV, Modules: []*Module{m}}
+
+		if vv.CallSink != 0 {
+			k := key{id: vv.OSV.ID, pkg: vv.PkgPath, mod: vv.ModPath}
+			vcs := uniqueCallStack(vv, callStacks[vv], vulnsPerPkg[k], vr)
+			if vcs != nil {
+				cs := CallStack{
+					Frames: stackFramesfromEntries(vcs),
+					Symbol: vv.Symbol,
+				}
+				cs.Summary = SummarizeCallStack(cs, topPkgs, p.Path)
+				p.CallStacks = []CallStack{cs}
+			}
+		}
+		r.Vulns = append(r.Vulns, v)
+	}
+
+	r = merge(r)
+	sortResult(r)
+	return r
+}
+
+func createBinaryResult(vr *vulncheck.Result) *Result {
+	modVersions := moduleVersionMap(vr.Modules)
+	// Create Result where each vulncheck.Vuln{OSV, ModPath, PkgPath} becomes
+	// a separate Vuln{OSV, Modules{Packages{PkgPath}}} entry. We merge the
+	// results later.
+	r := &Result{}
+	for _, vv := range vr.Vulns {
+		p := &Package{Path: vv.PkgPath}
+		m := &Module{
+			Path:         vv.ModPath,
+			FoundVersion: foundVersion(vv.ModPath, modVersions),
+			FixedVersion: fixedVersion(vv.ModPath, vv.OSV.Affected),
+			Packages:     []*Package{p},
+		}
+		v := &Vuln{OSV: vv.OSV, Modules: []*Module{m}}
+		r.Vulns = append(r.Vulns, v)
+	}
+
+	r = merge(r)
+	sortResult(r)
+	return r
+}
+
+// merge takes r and creates a Result where duplicate
+// vulns, modules, and packages are merged together.
+// For instance, Vulns with the same OSV field are
+// merged into a single one. The same applies for
+// Modules of a Vuln, and Packages of a Module.
+func merge(r *Result) *Result {
+	nr := &Result{}
+	// merge vulns by their ID. Note that there can
+	// be several OSVs with the same ID but different
+	// pointer values
+	osvs := make(map[string]*osv.Entry)
+	vs := make(map[string][]*Module)
+	for _, v := range r.Vulns {
+		osvs[v.OSV.ID] = v.OSV
+		vs[v.OSV.ID] = append(vs[v.OSV.ID], v.Modules...)
+	}
+
+	for id, mods := range vs {
+		v := &Vuln{OSV: osvs[id], Modules: mods}
+		nr.Vulns = append(nr.Vulns, v)
+	}
+
+	// merge modules
+	for _, v := range nr.Vulns {
+		ms := make(map[string][]*Module)
+		for _, m := range v.Modules {
+			ms[m.Path] = append(ms[m.Path], m)
+		}
+
+		var nms []*Module
+		for mpath, mods := range ms {
+			// modules with the same path must have
+			// same found and fixed versions
+			validateModuleVersions(mods)
+			nm := &Module{
+				Path:         mpath,
+				FixedVersion: mods[0].FixedVersion,
+				FoundVersion: mods[0].FoundVersion,
+			}
+			for _, mod := range mods {
+				nm.Packages = append(nm.Packages, mod.Packages...)
+			}
+			nms = append(nms, nm)
+		}
+		v.Modules = nms
+	}
+
+	// merge packages
+	for _, v := range nr.Vulns {
+		for _, m := range v.Modules {
+			ps := make(map[string][]*Package)
+			for _, p := range m.Packages {
+				ps[p.Path] = append(ps[p.Path], p)
+			}
+
+			var nps []*Package
+			for ppath, pkgs := range ps {
+				np := &Package{Path: ppath}
+				for _, p := range pkgs {
+					np.CallStacks = append(np.CallStacks, p.CallStacks...)
+				}
+				nps = append(nps, np)
+			}
+			m.Packages = nps
+		}
+	}
+	return nr
+}
+
+// validateModuleVersions checks that all modules have
+// the same found and fixed version. If not, panics.
+func validateModuleVersions(modules []*Module) {
+	var found, fixed string
+	for i, m := range modules {
+		if i == 0 {
+			found = m.FoundVersion
+			fixed = m.FixedVersion
+			continue
+		}
+		if m.FoundVersion != found || m.FixedVersion != fixed {
+			panic(fmt.Sprintf("found or fixed version incompatible for module %s", m.Path))
+		}
+	}
+}
+
+// sortResults sorts Vulns, Modules, and Packages of r.
+func sortResult(r *Result) {
+	sort.Slice(r.Vulns, func(i, j int) bool {
+		return r.Vulns[i].OSV.ID > r.Vulns[j].OSV.ID
+	})
+	for _, v := range r.Vulns {
+		sort.Slice(v.Modules, func(i, j int) bool {
+			return v.Modules[i].Path < v.Modules[j].Path
+		})
+		for _, m := range v.Modules {
+			sort.Slice(m.Packages, func(i, j int) bool {
+				return m.Packages[i].Path < m.Packages[j].Path
+			})
+		}
+	}
+}
+
+// stackFramesFromEntries creates a sequence of stack
+// frames from vcs. Position of a StackFrame is the
+// call position of the corresponding stack entry.
+func stackFramesfromEntries(vcs vulncheck.CallStack) []*StackFrame {
+	var frames []*StackFrame
+	for _, e := range vcs {
+		fr := &StackFrame{
+			FuncName: e.Function.Name,
+			PkgPath:  e.Function.PkgPath,
+			RecvType: e.Function.RecvType,
+		}
+		if e.Call == nil || e.Call.Pos == nil {
+			fr.Position = token.Position{}
+		} else {
+			fr.Position = *e.Call.Pos
+		}
+		frames = append(frames, fr)
+	}
+	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 71fc2d4..4a6481d 100644
--- a/internal/govulncheck/run_test.go
+++ b/internal/govulncheck/run_test.go
@@ -191,6 +191,17 @@
 	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 {
@@ -200,21 +211,21 @@
 	}
 
 	// V1, V2, and V3 are vulnerable symbols
-	skip := map[*vulncheck.FuncNode]bool{v1: true, v2: true, v3: true}
+	skip := []*vulncheck.Vuln{vuln1, vuln2, vuln3}
 	for _, test := range []struct {
-		v    *vulncheck.FuncNode
+		vuln *vulncheck.Vuln
 		css  []vulncheck.CallStack
 		want vulncheck.CallStack
 	}{
 		// [A -> B -> V3 -> V1, A -> V1] ==> A -> V1 since the first stack goes through V3
-		{v1, []vulncheck.CallStack{callStack(a, b, v3, v1), callStack(a, v1)}, callStack(a, v1)},
+		{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
-		{v2, []vulncheck.CallStack{callStack(a, v1, v2)}, nil},
+		{vuln2, []vulncheck.CallStack{callStack(a, v1, v2)}, nil},
 		// [A -> V1 -> V3, A -> B -> v3] ==> A -> B -> V3 since the first stack goes through V1
-		{v3, []vulncheck.CallStack{callStack(a, v1, v3), callStack(a, b, v3)}, callStack(a, b, v3)},
+		{vuln3, []vulncheck.CallStack{callStack(a, v1, v3), callStack(a, b, v3)}, callStack(a, b, v3)},
 	} {
-		t.Run(test.v.Name, func(t *testing.T) {
-			got := uniqueCallStack(test.v, test.css, skip)
+		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)
 			}
diff --git a/internal/govulncheck/source.go b/internal/govulncheck/source.go
deleted file mode 100644
index 9c8570a..0000000
--- a/internal/govulncheck/source.go
+++ /dev/null
@@ -1,240 +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 (
-	"fmt"
-	"go/ast"
-	"go/token"
-	"sort"
-	"strconv"
-	"strings"
-
-	"golang.org/x/tools/go/packages"
-	"golang.org/x/vuln/vulncheck"
-)
-
-// 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
-}
-
-// callInfo is information about calls to vulnerable functions.
-type callInfo struct {
-	// callStacks contains all call stacks to vulnerable functions.
-	callStacks map[*vulncheck.Vuln][]vulncheck.CallStack
-
-	// vulnGroups contains vulnerabilities grouped by ID and package.
-	vulnGroups [][]*vulncheck.Vuln
-
-	// moduleVersions is a map of module paths to versions.
-	moduleVersions map[string]string
-
-	// topPackages contains the top-level packages in the call info.
-	topPackages map[string]bool
-}
-
-// getCallInfo computes call stacks and related information from a vulncheck.Result.
-// It also makes a set of top-level packages from pkgs.
-func getCallInfo(r *vulncheck.Result, pkgs []*vulncheck.Package) *callInfo {
-	pset := map[string]bool{}
-	for _, p := range pkgs {
-		pset[p.PkgPath] = true
-	}
-	cs := vulncheck.CallStacks(r)
-	updateInitPositions(cs, pkgs)
-	return &callInfo{
-		callStacks:     cs,
-		vulnGroups:     groupByIDAndPackage(r.Vulns),
-		moduleVersions: moduleVersionMap(r.Modules),
-		topPackages:    pset,
-	}
-}
-
-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
-}
-
-// 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
-}
-
-// updateInitPositions populates non-existing positions of init functions
-// and their respective calls in callStacks (see #51575).
-func updateInitPositions(callStacks map[*vulncheck.Vuln][]vulncheck.CallStack, pkgs []*vulncheck.Package) {
-	pMap := pkgMap(pkgs)
-	for _, css := range callStacks {
-		for _, cs := range css {
-			for i, _ := range cs {
-				updateInitPosition(&cs[i], pMap)
-				if i != len(cs)-1 {
-					updateInitCallPosition(&cs[i], cs[i+1], pMap)
-				}
-			}
-		}
-	}
-}
-
-// updateInitCallPosition updates the position of a call to init in a stack frame, if
-// one already does not exist:
-//
-//	P1.init -> P2.init: position of call to P2.init is the position of "import P2"
-//	statement in P1
-//
-//	P.init -> P.init#d: P.init is an implicit init. We say it calls the explicit
-//	P.init#d at the place of "package P" statement.
-func updateInitCallPosition(curr *vulncheck.StackEntry, next vulncheck.StackEntry, pkgs map[string]*vulncheck.Package) {
-	call := curr.Call
-	if !isInit(next.Function) || (call.Pos != nil && call.Pos.IsValid()) {
-		// Skip non-init functions and inits whose call site position is available.
-		return
-	}
-
-	pkg := pkgs[curr.Function.PkgPath]
-	var pos token.Position
-	if curr.Function.Name == "init" && curr.Function.PkgPath == next.Function.PkgPath {
-		// We have implicit P.init calling P.init#d. Set the call position to
-		// be at "package P" statement position.
-		pos = packageStatementPos(pkg)
-	} else {
-		// Choose the beginning of the import statement as the position.
-		pos = importStatementPos(pkg, next.Function.PkgPath)
-	}
-
-	call.Pos = &pos
-}
-
-func importStatementPos(pkg *vulncheck.Package, importPath string) token.Position {
-	var importSpec *ast.ImportSpec
-spec:
-	for _, f := range pkg.Syntax {
-		for _, impSpec := range f.Imports {
-			// Import spec paths have quotation marks.
-			impSpecPath, err := strconv.Unquote(impSpec.Path.Value)
-			if err != nil {
-				panic(fmt.Sprintf("import specification: package path has no quotation marks: %v", err))
-			}
-			if impSpecPath == importPath {
-				importSpec = impSpec
-				break spec
-			}
-		}
-	}
-
-	if importSpec == nil {
-		// for sanity, in case of a wild call graph imprecision
-		return token.Position{}
-	}
-
-	// Choose the beginning of the import statement as the position.
-	return pkg.Fset.Position(importSpec.Pos())
-}
-
-func packageStatementPos(pkg *vulncheck.Package) token.Position {
-	if len(pkg.Syntax) == 0 {
-		return token.Position{}
-	}
-	// Choose beginning of the package statement as the position. Pick
-	// the first file since it is as good as any.
-	return pkg.Fset.Position(pkg.Syntax[0].Package)
-}
-
-// updateInitPosition updates the position of P.init function in a stack frame if one
-// is not available. The new position is the position of the "package P" statement.
-func updateInitPosition(se *vulncheck.StackEntry, pkgs map[string]*vulncheck.Package) {
-	fun := se.Function
-	if !isInit(fun) || (fun.Pos != nil && fun.Pos.IsValid()) {
-		// Skip non-init functions and inits whose position is available.
-		return
-	}
-
-	pos := packageStatementPos(pkgs[fun.PkgPath])
-	fun.Pos = &pos
-}
-
-func isInit(f *vulncheck.FuncNode) bool {
-	// A source init function, or anonymous functions used in inits, will
-	// be named "init#x" by vulncheck (more precisely, ssa), where x is a
-	// positive integer. Implicit inits are named simply "init".
-	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
-
-		for _, i := range p.Imports {
-			visit(i)
-		}
-	}
-
-	for _, p := range pkgs {
-		visit(p)
-	}
-	return m
-}
diff --git a/internal/govulncheck/stdlib.go b/internal/govulncheck/stdlib.go
index 8935f3e..51b2303 100644
--- a/internal/govulncheck/stdlib.go
+++ b/internal/govulncheck/stdlib.go
@@ -11,6 +11,8 @@
 	"golang.org/x/mod/semver"
 )
 
+// TODO: move this to util.go
+
 // Support functions for standard library packages.
 // These are copied from the internal/stdlib package in the pkgsite repo.
 
diff --git a/internal/govulncheck/summary.go b/internal/govulncheck/summary.go
deleted file mode 100644
index 0cd8d2b..0000000
--- a/internal/govulncheck/summary.go
+++ /dev/null
@@ -1,106 +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/vuln/osv"
-	"golang.org/x/vuln/vulncheck"
-)
-
-// LegacySummary is the govulncheck result.
-//
-// TODO(https://go.dev/issue/56042): replace LegacySummary with Result
-type LegacySummary struct {
-	// Vulnerabilities affecting the analysis target binary or source code.
-	Affecting []LegacyVuln
-	// Vulnerabilities that may be imported but the vulnerable symbols are
-	// not called. For binary analysis, this will be always empty.
-	NonAffecting []LegacyVuln
-}
-
-// LegacyVuln represents a vulnerability relevant to a (module, package).
-type LegacyVuln struct {
-	OSV     *osv.Entry
-	PkgPath string // Package path.
-	ModPath string // Module path.
-	FoundIn string // Module version used in the analyzed code. Empty if unknown.
-	FixedIn string // Fixed version if fix is available. Empty otherwise.
-	// Trace contains a call stack for each affecting symbol.
-	// For vulnerabilities found from binary analysis, and vulnerabilities
-	// that are reported as Unaffecting ones, this will be always empty.
-	Trace []LegacyTrace
-}
-
-// LegacyTrace represents a sample trace for a vulnerable symbol.
-type LegacyTrace struct {
-	Symbol string             // Name of the detected vulnerable function or method.
-	Desc   string             // One-line description of the callstack.
-	Stack  []LegacyStackEntry // Call stack.
-	Seen   int                // Number of similar call stacks.
-}
-
-// LegacyStackEntry represents a call stack entry.
-type LegacyStackEntry struct {
-	FuncName string // Function name is the function name, adjusted to remove pointer annotation.
-	CallSite string // Position of the call/reference site. It is one of the formats token.Pos.String() returns or empty if unknown.
-}
-
-// summary summarize the analysis result.
-func summary(ci *callInfo, unaffected []*vulncheck.Vuln) LegacySummary {
-	var affecting, unaffecting []LegacyVuln
-	for _, vg := range ci.vulnGroups {
-		// All the vulns in vg have the same PkgPath, ModPath and OSV.
-		// All have a non-zero CallSink.
-		v0 := vg[0]
-		stacks := summarizeCallStacks(vg, ci)
-
-		affecting = append(affecting, LegacyVuln{
-			OSV:     vg[0].OSV,
-			PkgPath: v0.PkgPath,
-			ModPath: v0.ModPath,
-			FoundIn: foundVersion(v0.ModPath, ci),
-			FixedIn: fixedVersion(v0.ModPath, v0.OSV.Affected),
-			Trace:   stacks,
-		})
-	}
-	for _, vuln := range unaffected {
-		unaffecting = append(unaffecting, LegacyVuln{
-			OSV:     vuln.OSV,
-			PkgPath: vuln.PkgPath,
-			ModPath: vuln.ModPath,
-			FoundIn: foundVersion(vuln.ModPath, ci),
-			FixedIn: fixedVersion(vuln.ModPath, vuln.OSV.Affected),
-		})
-	}
-	return LegacySummary{
-		Affecting:    affecting,
-		NonAffecting: unaffecting,
-	}
-}
-
-func summarizeCallStacks(vg []*vulncheck.Vuln, ci *callInfo) []LegacyTrace {
-	cs := make([]LegacyTrace, 0, len(vg))
-	// report one full call stack for each vuln.
-	for _, v := range vg {
-		css := ci.callStacks[v]
-		if len(css) == 0 {
-			continue
-		}
-		stack := make([]LegacyStackEntry, 0, len(css))
-		for _, e := range css[0] {
-			stack = append(stack, LegacyStackEntry{
-				FuncName: FuncName(e.Function),
-				CallSite: FuncPos(e.Call),
-			})
-		}
-		cs = append(cs, LegacyTrace{
-			Symbol: v.Symbol,
-			Desc:   SummarizeCallStack(css[0], ci.topPackages, v.PkgPath),
-			Stack:  stack,
-			Seen:   len(css),
-		})
-	}
-	return cs
-}
diff --git a/internal/govulncheck/util.go b/internal/govulncheck/util.go
index 17cc23c..3c750a3 100644
--- a/internal/govulncheck/util.go
+++ b/internal/govulncheck/util.go
@@ -9,6 +9,7 @@
 	"strings"
 
 	"golang.org/x/mod/semver"
+	"golang.org/x/vuln/internal"
 	isem "golang.org/x/vuln/internal/semver"
 	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
@@ -16,6 +17,8 @@
 
 // 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 {
 	v := ""
 	for _, a := range as {
@@ -33,6 +36,35 @@
 	return v
 }
 
+func foundVersion(modulePath string, moduleVersions map[string]string) string {
+	var found string
+	if v := moduleVersions[modulePath]; v != "" {
+		found = versionString(modulePath, v[1:])
+	}
+	return found
+}
+
+func fixedVersion(modulePath string, affected []osv.Affected) string {
+	fixed := LatestFixed(affected)
+	if fixed != "" {
+		fixed = versionString(modulePath, fixed)
+	}
+	return fixed
+}
+
+// versionString prepends a version string prefix (`v` or `go`
+// depending on the modulePath) to the given semver-style version string.
+func versionString(modulePath, version string) string {
+	if version == "" {
+		return ""
+	}
+	v := "v" + version
+	if modulePath == internal.GoStdModulePath || modulePath == internal.GoCmdModulePath {
+		return semverToGoTag(v)
+	}
+	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:
@@ -42,38 +74,42 @@
 //     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 {
+//
+// 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, func(e vulncheck.StackEntry) bool {
-		return topPkgs[PkgPath(e.Function)]
+	iTop := lowest(cs.Frames, func(e *StackFrame) bool {
+		return topPkgs[e.PkgPath]
 	})
 	if iTop < 0 {
+		print("1\n")
 		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
+	iVuln := highest(cs.Frames[iTop+1:], func(e *StackFrame) bool {
+		return e.PkgPath == vulnPkg
 	})
 	if iVuln < 0 {
+		print("2\n")
 		return ""
 	}
 	iVuln += iTop + 1 // adjust for slice in call to highest.
-	topName := FuncName(cs[iTop].Function)
-	topPos := AbsRelShorter(FuncPos(cs[iTop].Call))
+	topName := funcName(cs.Frames[iTop])
+	topPos := AbsRelShorter(funcPos(cs.Frames[iTop]))
 	if topPos != "" {
 		topPos += ": "
 	}
-	vulnName := FuncName(cs[iVuln].Function)
+	vulnName := funcName(cs.Frames[iVuln])
 	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[iTop+1].Function), vulnName)
+		topPos, topName, funcName(cs.Frames[iTop+1]), 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 {
+func highest(cs []*StackFrame, f func(e *StackFrame) bool) int {
 	for i := 0; i < len(cs); i++ {
 		if f(cs[i]) {
 			return i
@@ -84,7 +120,7 @@
 
 // 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 {
+func lowest(cs []*StackFrame, f func(e *StackFrame) bool) int {
 	for i := len(cs) - 1; i >= 0; i-- {
 		if f(cs[i]) {
 			return i
@@ -94,6 +130,8 @@
 }
 
 // PkgPath returns the package path from fn.
+//
+// TODO: make private
 func PkgPath(fn *vulncheck.FuncNode) string {
 	if fn.PkgPath != "" {
 		return fn.PkgPath
@@ -105,16 +143,23 @@
 	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(), "*")
+// 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 function position from call.
-func FuncPos(call *vulncheck.CallSite) string {
-	if call != nil && call.Pos != nil {
-		return call.Pos.String()
+// 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 ""
 }
diff --git a/internal/govulncheck/util_test.go b/internal/govulncheck/util_test.go
index 67e7772..6bd4c46 100644
--- a/internal/govulncheck/util_test.go
+++ b/internal/govulncheck/util_test.go
@@ -67,15 +67,13 @@
 	}
 }
 
-func stringToCallStack(s string) vulncheck.CallStack {
-	var cs vulncheck.CallStack
+func stringToCallStack(s string) CallStack {
+	var cs 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],
-			},
+		cs.Frames = append(cs.Frames, &StackFrame{
+			PkgPath:  parts[0],
+			FuncName: parts[1],
 		})
 	}
 	return cs