codegen/reflect_interface.go (111 lines of code) (raw):
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package codegen
import (
"bytes"
"encoding/gob"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"text/template"
"github.com/golang/mock/mockgen/model"
"github.com/pkg/errors"
)
type reflectData struct {
PathAliasMap map[string]string
PathSymbolMap map[string]string
}
// ReflectInterface uses reflection to obtain interface information for each path symbol pair in the pathSympolMap
// projRoot is the root dir where mockgen is installed as a vendor package
func ReflectInterface(projRoot string, pathSymbolMap map[string]string) (map[string]*model.Package, error) {
// We use TempDir instead of TempFile so we can control the filename.
tmpDir, err := ioutil.TempDir("./", "gomock_reflect_")
if err != nil {
return nil, err
}
defer func() { _ = os.RemoveAll(tmpDir) }()
const progSource = "prog.go"
var progBinary = "prog.bin"
if runtime.GOOS == "windows" {
// Windows won't execute a program unless it has a ".exe" suffix.
progBinary += ".exe"
}
// Generate program
paths := make(map[string]bool, len(pathSymbolMap))
for p := range pathSymbolMap {
paths[p] = true
}
data := reflectData{
PathAliasMap: uniqueAlias(paths),
PathSymbolMap: pathSymbolMap,
}
var program bytes.Buffer
if err := reflectProgram.Execute(&program, &data); err != nil {
return nil, err
}
if err := ioutil.WriteFile(filepath.Join(tmpDir, progSource), program.Bytes(), 0600); err != nil {
return nil, err
}
// Build it
var buildStdout, buildStderr bytes.Buffer
build := exec.Command("go", "build", "-o", progBinary, progSource)
build.Dir = tmpDir
build.Stdout = &buildStdout
build.Stderr = &buildStderr
if err := build.Run(); err != nil {
return nil, errors.Wrap(err, buildStderr.String())
}
progPath := filepath.Join(tmpDir, progBinary)
// Run it
var stdout, stderr bytes.Buffer
cmd := exec.Command(progPath)
cmd.Dir = projRoot
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, errors.Wrap(err, stderr.String())
}
var pkgs map[string]*model.Package
if err := gob.NewDecoder(&stdout).Decode(&pkgs); err != nil {
return nil, err
}
return pkgs, nil
}
// This program reflects on an interface value, and prints the
// gob encoding of a model.Package to standard output.
// JSON doesn't work because of the model.Type interface.
var reflectProgram = template.Must(template.New("program").Parse(`
{{$pathAliasMap := .PathAliasMap}}
{{$pathSymbolMap := .PathSymbolMap}}
package main
import (
"encoding/gob"
"fmt"
"os"
"reflect"
"github.com/golang/mock/mockgen/model"
{{range $importPath, $alias := $pathAliasMap}}
{{$alias}} "{{$importPath}}"
{{end}}
)
func main() {
its := []struct{
path, sym string
typ reflect.Type
}{
{{range $importPath, $symbol := $pathSymbolMap}}
{"{{$importPath}}", "{{$symbol}}", reflect.TypeOf((*{{index $pathAliasMap $importPath}}.{{$symbol}})(nil)).Elem()},
{{end}}
}
pkgs := make(map[string]*model.Package, {{len $pathSymbolMap}})
{{range $importPath, $symbol := $pathSymbolMap}}
pkgs["{{$importPath}}"] = &model.Package{
Name: "{{index $pathAliasMap $importPath}}",
}
{{end}}
stderr := os.Stderr
for _, it := range its {
intf, err := model.InterfaceFromInterfaceType(it.typ)
if err != nil {
fmt.Fprintf(stderr, "Reflection: %v\n", err)
os.Exit(1)
}
intf.Name = it.sym
pkgs[it.path].Interfaces = []*model.Interface{intf}
}
if err := gob.NewEncoder(os.Stdout).Encode(pkgs); err != nil {
fmt.Fprintf(stderr, "gob encode: %v\n", err)
os.Exit(1)
}
}
`))