Skip to content

Commit

Permalink
support gcexportdata export files for loading packages instead of `go…
Browse files Browse the repository at this point in the history
…lang.org/x/tools/go/packages.Load`
  • Loading branch information
Strum355 committed Feb 12, 2024
1 parent fb9effb commit 87f69a6
Show file tree
Hide file tree
Showing 14 changed files with 395 additions and 46 deletions.
91 changes: 91 additions & 0 deletions cmd/go-mockgen/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"path/filepath"
"strings"

"github.com/derision-test/go-mockgen/internal"
)

type archive struct {
// ImportMap refers to the actual import path to the library this archive represents.
// See https://github.com/bazelbuild/rules_go/blob/a9b312afd2866f4316356b456df1971bff6cd244/go/core.rst#go_library.
ImportMap string
File string
}

// The following is the format expected by this function:
//
// IMPORTMAP=EXPORT e.g. github.com/foo/bar=bar_export.a
//
// The flag is structured in this format to loosely follow https://sourcegraph.com/github.com/bazelbuild/rules_go@a9b312afd2866f4316356b456df1971bff6cd244/-/blob/go/private/actions/compilepkg.bzl?L22-29;
// however, the IMPORTPATHS section is omitted. There may be future
// work involved in resolving import aliases/vendoring using IMPORTPATHS.
func parseArchive(a string) (archive, error) {
args := strings.Split(a, "=")
if len(args) != 2 {
return archive{}, fmt.Errorf("expected 2 elements, got %d: %v", len(args), a)
}

return archive{
ImportMap: args[0],
File: args[1],
}, nil
}

func PackagesArchive(p loadParams) (packages []*internal.GoPackage, err error) {
fset := token.NewFileSet()
for _, importpath := range p.importPaths {
files := make([]*ast.File, 0, len(p.sources))
for _, src := range p.sources[importpath] {
if ok, err := build.Default.MatchFile(filepath.Dir(src), filepath.Base(src)); err != nil {
return nil, fmt.Errorf("error checking if file matches constraints: %w", err)
} else if !ok || filepath.Ext(src) == ".s" {
fmt.Printf("skipping %q\n", src)
continue
}

f, err := parser.ParseFile(fset, src, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("error parsing %q: %v", src, err)
}

files = append(files, f)
}

imp, err := newImporter(fset, p.archives, p.stdlibRoot)
if err != nil {
return nil, err
}
conf := types.Config{Importer: imp, Error: func(err error) {
fmt.Println(err)
}}
typesInfo := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Implicits: make(map[ast.Node]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
}

pkg, err := conf.Check(importpath, fset, files, typesInfo)
if err != nil {
return nil, fmt.Errorf("error building pkg %q: %w", importpath, err)
}
packages = append(packages, &internal.GoPackage{
PkgPath: pkg.Path(),
CompiledGoFiles: p.sources[importpath],
Syntax: files,
Types: pkg,
TypesInfo: typesInfo,
})
}
return
}
54 changes: 41 additions & 13 deletions cmd/go-mockgen/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ func parseAndValidateOptions() ([]*generation.Options, error) {

func parseOptions() ([]*generation.Options, error) {
if len(os.Args) == 1 {
return parseManifest()
return parseManifest("")
}

opts, err := parseFlags()
if err != nil {
return nil, err
}

if opts.ManifestDir != "" {
return parseManifest(opts.ManifestDir)
}

return []*generation.Options{opts}, nil
}

Expand All @@ -67,7 +71,7 @@ func parseFlags() (*generation.Options, error) {
app := kingpin.New(consts.Name, consts.Description).Version(consts.Version)
app.UsageWriter(os.Stdout)

app.Arg("path", "The import paths used to search for eligible interfaces").Required().StringsVar(&opts.PackageOptions[0].ImportPaths)
app.Arg("path", "The import paths used to search for eligible interfaces").StringsVar(&opts.PackageOptions[0].ImportPaths)
app.Flag("package", "The name of the generated package. It will be inferred from the output options by default.").Short('p').StringVar(&opts.ContentOptions.PkgName)
app.Flag("interfaces", "A list of target interfaces to generate defined in the given the import paths.").Short('i').StringsVar(&opts.PackageOptions[0].Interfaces)
app.Flag("exclude", "A list of interfaces to exclude from generation. Mocks for all other exported interfaces defined in the given import paths are generated.").Short('e').StringsVar(&opts.PackageOptions[0].Exclude)
Expand All @@ -82,6 +86,7 @@ func parseFlags() (*generation.Options, error) {
app.Flag("for-test", "Append _test suffix to generated package names and file names.").Default("false").BoolVar(&opts.OutputOptions.ForTest)
app.Flag("file-prefix", "Content that is written at the top of each generated file.").StringVar(&opts.ContentOptions.FilePrefix)
app.Flag("build-constraints", "Build constraints that are added to each generated file.").StringVar(&opts.ContentOptions.BuildConstraints)
app.Flag("manifest-dir", "Dir in which to search for the root mockgen.yaml file in. All other flags are ignored if this is set, and config is taken from the manifest file(s).").StringVar(&opts.ManifestDir)

if _, err := app.Parse(os.Args[1:]); err != nil {
return nil, err
Expand All @@ -90,8 +95,8 @@ func parseFlags() (*generation.Options, error) {
return opts, nil
}

func parseManifest() ([]*generation.Options, error) {
payload, err := readManifest()
func parseManifest(manifestDir string) ([]*generation.Options, error) {
payload, err := readManifest(manifestDir)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -126,6 +131,10 @@ func parseManifest() ([]*generation.Options, error) {
opts.ForTest = true
}

if len(opts.Paths) > 0 && len(opts.Archives) > 0 {
return nil, fmt.Errorf("multiple import paths and archives are mutually exclusive")
}

// Canonicalization
paths := opts.Paths
if opts.Path != "" {
Expand All @@ -139,11 +148,15 @@ func parseManifest() ([]*generation.Options, error) {

var packageOptions []generation.PackageOptions
if len(opts.Sources) > 0 {
if len(opts.Paths) > 0 || len(opts.Interfaces) > 0 {
if len(opts.Paths) > 0 || len(opts.Interfaces) > 0 || opts.Path != "" {
return nil, fmt.Errorf("sources and path/paths/interfaces are mutually exclusive")
}

for _, source := range opts.Sources {
if len(source.Paths) > 0 && len(opts.Archives) > 0 {
return nil, fmt.Errorf("multiple import paths and archives are mutually exclusive")
}

// Canonicalization
paths := source.Paths
if source.Path != "" {
Expand All @@ -155,6 +168,9 @@ func parseManifest() ([]*generation.Options, error) {
Interfaces: source.Interfaces,
Exclude: source.Exclude,
Prefix: source.Prefix,
Archives: opts.Archives,
SourceFiles: source.SourceFiles,
StdlibRoot: payload.StdlibRoot,
})
}
} else {
Expand All @@ -163,6 +179,9 @@ func parseManifest() ([]*generation.Options, error) {
Interfaces: opts.Interfaces,
Exclude: opts.Exclude,
Prefix: opts.Prefix,
Archives: opts.Archives,
SourceFiles: opts.SourceFiles,
StdlibRoot: payload.StdlibRoot,
})
}

Expand Down Expand Up @@ -203,13 +222,17 @@ type yamlPayload struct {
ForTest bool `yaml:"for-test"`
FilePrefix string `yaml:"file-prefix"`

StdlibRoot string `yaml:"stdlib-root"`

Mocks []yamlMock `yaml:"mocks"`
}

type yamlMock struct {
Path string `yaml:"path"`
Paths []string `yaml:"paths"`
Sources []yamlSource `yaml:"sources"`
SourceFiles []string `yaml:"source-files"`
Archives []string `yaml:"archives"`
Package string `yaml:"package"`
Interfaces []string `yaml:"interfaces"`
Exclude []string `yaml:"exclude"`
Expand All @@ -226,15 +249,16 @@ type yamlMock struct {
}

type yamlSource struct {
Path string `yaml:"path"`
Paths []string `yaml:"paths"`
Interfaces []string `yaml:"interfaces"`
Exclude []string `yaml:"exclude"`
Prefix string `yaml:"prefix"`
Path string `yaml:"path"`
Paths []string `yaml:"paths"`
Interfaces []string `yaml:"interfaces"`
Exclude []string `yaml:"exclude"`
Prefix string `yaml:"prefix"`
SourceFiles []string `yaml:"source-files"`
}

func readManifest() (yamlPayload, error) {
contents, err := os.ReadFile("mockgen.yaml")
func readManifest(manifestDir string) (yamlPayload, error) {
contents, err := os.ReadFile(filepath.Join(manifestDir, "mockgen.yaml"))
if err != nil {
return yamlPayload{}, err
}
Expand All @@ -245,7 +269,7 @@ func readManifest() (yamlPayload, error) {
}

for _, path := range payload.IncludeConfigPaths {
payload, err = readIncludeConfig(payload, path)
payload, err = readIncludeConfig(payload, filepath.Join(manifestDir, path))
if err != nil {
return yamlPayload{}, err
}
Expand Down Expand Up @@ -307,6 +331,10 @@ var goIdentifierPattern = regexp.MustCompile("^[A-Za-z]([A-Za-z0-9_]*)?$")

func validateOptions(opts *generation.Options) (bool, error) {
for _, packageOpts := range opts.PackageOptions {
if len(packageOpts.ImportPaths) == 0 {
return false, fmt.Errorf("missing interface source import paths")
}

if len(packageOpts.Interfaces) != 0 && len(packageOpts.Exclude) != 0 {
return false, fmt.Errorf("interface lists and exclude lists are mutually exclusive")
}
Expand Down
79 changes: 79 additions & 0 deletions cmd/go-mockgen/importer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package main

import (
"fmt"
"go/token"
"go/types"
"os"
"strings"

"golang.org/x/tools/go/gcexportdata"
)

type importer struct {
importToArchive map[string]string
stdlibRoot string
fset *token.FileSet
imports map[string]*types.Package
}

func newImporter(fset *token.FileSet, archives []archive, root string) (types.Importer, error) {
imp := &importer{
importToArchive: make(map[string]string, len(archives)),
fset: fset,
imports: make(map[string]*types.Package),
stdlibRoot: root,
}

for _, archive := range archives {
imp.importToArchive[archive.ImportMap] = archive.File
}
return imp, nil
}

func (i *importer) Import(path string) (*types.Package, error) {
if pkg, ok := i.imports[path]; ok && pkg.Complete() {
return pkg, nil
}

if path == "unsafe" {
// Special case: go/types has pre-defined type information for unsafe.
// See https://github.com/golang/go/issues/13882.
return types.Unsafe, nil
}

if isStdlibImport(path) {
archiveFile := fmt.Sprintf("%v/%v.a", i.stdlibRoot, path)
return i.readArchive(archiveFile, path)
}

if archive, ok := i.importToArchive[path]; ok {
return i.readArchive(archive, path)
}
return nil, fmt.Errorf("package %q not found in read archives: please double check dependencies for the go-mockgen bazel rule", path)
}

func (i *importer) readArchive(archiveFile, path string) (p *types.Package, err error) {
f, err := os.Open(archiveFile)
if err != nil {
return nil, err
}
defer func() { f.Close() }()

r, err := gcexportdata.NewReader(f)
if err != nil {
return nil, err
}

return gcexportdata.Read(r, i.fset, i.imports, path)
}

func isStdlibImport(path string) bool {
if i := strings.IndexByte(path, '/'); i >= 0 {
path = path[:i]
}

// If the prefix of the import path contains a ".", it should be considered
// to be a external package (not part of Go standard lib).
return !strings.Contains(path, ".")
}
Loading

0 comments on commit 87f69a6

Please sign in to comment.