vulncheck: introduce vulncheck.Package

This change introduces vulncheck.Package representation of input
packages and adds conversion API from packages.Package.

This change will be followed by a change that replaces uses of
packages.Package with those of vulncheck.Package.

Change-Id: I4e72bc8e934ebca86d3039ae27ae05820b73d9d6
Reviewed-on: https://go-review.googlesource.com/c/exp/+/369134
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
Trust: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/vulncheck/helpers_test.go b/vulncheck/helpers_test.go
index 58afe38..1c63f8e 100644
--- a/vulncheck/helpers_test.go
+++ b/vulncheck/helpers_test.go
@@ -132,6 +132,49 @@
 	return m
 }
 
+func pkgPathToImports(pkgs []*Package) map[string][]string {
+	m := make(map[string][]string)
+	seen := make(map[*Package]bool)
+	var visit func(*Package)
+	visit = func(p *Package) {
+		if seen[p] {
+			return
+		}
+		seen[p] = true
+		var imports []string
+		for _, i := range p.Imports {
+			imports = append(imports, i.PkgPath)
+			visit(i)
+		}
+		m[p.PkgPath] = imports
+	}
+	for _, p := range pkgs {
+		visit(p)
+	}
+	sortStrMap(m)
+	return m
+}
+
+func modulePathToVersion(pkgs []*Package) map[string]string {
+	m := make(map[string]string)
+	seen := make(map[*Package]bool)
+	var visit func(*Package)
+	visit = func(p *Package) {
+		if seen[p] || p.Module == nil {
+			return
+		}
+		seen[p] = true
+		for _, i := range p.Imports {
+			visit(i)
+		}
+		m[p.Module.Path] = p.Module.Version
+	}
+	for _, p := range pkgs {
+		visit(p)
+	}
+	return m
+}
+
 // sortStrMap sorts the map string slice values to make them deterministic.
 func sortStrMap(m map[string][]string) {
 	for _, strs := range m {
diff --git a/vulncheck/vulncheck.go b/vulncheck/vulncheck.go
index cd9b698..38b6c2c 100644
--- a/vulncheck/vulncheck.go
+++ b/vulncheck/vulncheck.go
@@ -8,7 +8,9 @@
 
 import (
 	"fmt"
+	"go/ast"
 	"go/token"
+	"go/types"
 	"strings"
 
 	"golang.org/x/tools/go/packages"
@@ -25,6 +27,80 @@
 	Client client.Client
 }
 
+// Package models Go package for vulncheck analysis. A version
+// of packages.Package trimmed down to reduce memory consumption.
+type Package struct {
+	Name      string
+	PkgPath   string
+	Imports   []*Package
+	Pkg       *types.Package
+	Fset      *token.FileSet
+	Syntax    []*ast.File
+	TypesInfo *types.Info
+	Module    *Module
+}
+
+// Module models Go module for vulncheck analysis.
+type Module struct {
+	Path    string
+	Version string
+	Dir     string
+	Replace *Module
+}
+
+// Convert converts a slice of packages.Package to
+// a slice of corresponding vulncheck.Package.
+func Convert(pkgs []*packages.Package) []*Package {
+	ms := make(map[*packages.Module]*Module)
+	var mod func(*packages.Module) *Module
+	mod = func(m *packages.Module) *Module {
+		if m == nil {
+			return nil
+		}
+		if vm, ok := ms[m]; ok {
+			return vm
+		}
+		vm := &Module{
+			Path:    m.Path,
+			Version: m.Version,
+			Dir:     m.Dir,
+			Replace: mod(m.Replace),
+		}
+		ms[m] = vm
+		return vm
+	}
+
+	ps := make(map[*packages.Package]*Package)
+	var pkg func(*packages.Package) *Package
+	pkg = func(p *packages.Package) *Package {
+		if vp, ok := ps[p]; ok {
+			return vp
+		}
+
+		vp := &Package{
+			Name:      p.Name,
+			PkgPath:   p.PkgPath,
+			Pkg:       p.Types,
+			Fset:      p.Fset,
+			Syntax:    p.Syntax,
+			TypesInfo: p.TypesInfo,
+			Module:    mod(p.Module),
+		}
+		ps[p] = vp
+
+		for _, i := range p.Imports {
+			vp.Imports = append(vp.Imports, pkg(i))
+		}
+		return vp
+	}
+
+	var vpkgs []*Package
+	for _, p := range pkgs {
+		vpkgs = append(vpkgs, pkg(p))
+	}
+	return vpkgs
+}
+
 // Result contains information on which vulnerabilities are potentially affecting
 // user code and how are they affecting it via call graph, package imports graph,
 // and module requires graph.
diff --git a/vulncheck/vulncheck_test.go b/vulncheck/vulncheck_test.go
index d4441bc..d68a1ee 100644
--- a/vulncheck/vulncheck_test.go
+++ b/vulncheck/vulncheck_test.go
@@ -5,10 +5,12 @@
 package vulncheck
 
 import (
+	"path"
 	"reflect"
 	"testing"
 
 	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/go/packages/packagestest"
 	"golang.org/x/vulndb/osv"
 )
 
@@ -216,3 +218,74 @@
 		t.Fatalf("VulnsForPackage returned unexpected results, got:\n%s\nwant:\n%s", vulnsToString(filtered), vulnsToString(expected))
 	}
 }
+
+func TestConvert(t *testing.T) {
+	e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+		{
+			Name: "golang.org/entry",
+			Files: map[string]interface{}{
+				"x/x.go": `
+			package x
+
+			import "golang.org/amod/avuln"
+		`}},
+		{
+			Name: "golang.org/zmod@v0.0.0",
+			Files: map[string]interface{}{"z/z.go": `
+			package z
+			`},
+		},
+		{
+			Name: "golang.org/amod@v1.1.3",
+			Files: map[string]interface{}{"avuln/avuln.go": `
+			package avuln
+
+			import "golang.org/wmod/w"
+			`},
+		},
+		{
+			Name: "golang.org/bmod@v0.5.0",
+			Files: map[string]interface{}{"bvuln/bvuln.go": `
+			package bvuln
+			`},
+		},
+		{
+			Name: "golang.org/wmod@v0.0.0",
+			Files: map[string]interface{}{"w/w.go": `
+			package w
+
+			import "golang.org/bmod/bvuln"
+			`},
+		},
+	})
+	defer e.Cleanup()
+
+	// Load x and y as entry packages.
+	pkgs, err := loadPackages(e, path.Join(e.Temp(), "entry/x"), path.Join(e.Temp(), "entry/y"))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	vpkgs := Convert(pkgs)
+
+	wantPkgs := map[string][]string{
+		"golang.org/amod/avuln": {"golang.org/wmod/w"},
+		"golang.org/bmod/bvuln": nil,
+		"golang.org/entry/x":    {"golang.org/amod/avuln"},
+		"golang.org/entry/y":    nil,
+		"golang.org/wmod/w":     {"golang.org/bmod/bvuln"},
+	}
+	if got := pkgPathToImports(vpkgs); !reflect.DeepEqual(got, wantPkgs) {
+		t.Errorf("want %v;got %v", wantPkgs, got)
+	}
+
+	wantMods := map[string]string{
+		"golang.org/amod":  "v1.1.3",
+		"golang.org/bmod":  "v0.5.0",
+		"golang.org/entry": "",
+		"golang.org/wmod":  "v0.0.0",
+	}
+	if got := modulePathToVersion(vpkgs); !reflect.DeepEqual(got, wantMods) {
+		t.Errorf("want %v;got %v", wantMods, got)
+	}
+}