Explicit conversion generator

This commit is contained in:
wojtekt 2019-10-08 12:32:44 +02:00
parent f08dfb4cb6
commit 5823fffc23
2 changed files with 239 additions and 28 deletions

View File

@ -21,6 +21,7 @@ import (
"fmt"
"io"
"path/filepath"
"reflect"
"sort"
"strings"
@ -41,6 +42,9 @@ const (
// e.g., "+k8s:conversion-gen=false" in a type's comment will let
// conversion-gen skip that type.
tagName = "k8s:conversion-gen"
// e.g. "+k8s:conversion-gen:explicit-from=net/url.Values" in the type comment
// will result in generating conversion from net/url.Values.
explicitFromTagName = "k8s:conversion-gen:explicit-from"
// e.g., "+k8s:conversion-gen-external-types=<type-pkg>" in doc.go, where
// <type-pkg> is the relative path to the package the types are defined in.
externalTypesTagName = "k8s:conversion-gen-external-types"
@ -50,6 +54,10 @@ func extractTag(comments []string) []string {
return types.ExtractCommentTags("+", comments)[tagName]
}
func extractExplicitFromTag(comments []string) []string {
return types.ExtractCommentTags("+", comments)[explicitFromTagName]
}
func extractExternalTypesTag(comments []string) []string {
return types.ExtractCommentTags("+", comments)[externalTypesTagName]
}
@ -240,14 +248,22 @@ func Packages(context *generator.Context, arguments *args.GeneratorArgs) generat
peerPkgs := extractTag(pkg.Comments)
if peerPkgs != nil {
klog.V(5).Infof(" tags: %q", peerPkgs)
if len(peerPkgs) == 1 && peerPkgs[0] == "false" {
// If a single +k8s:conversion-gen=false tag is defined, we still want
// the generator to fire for this package for explicit conversions, but
// we are clearing the peerPkgs to not generate any standard conversions.
peerPkgs = nil
}
} else {
klog.V(5).Infof(" no tag")
continue
}
skipUnsafe := false
if customArgs, ok := arguments.CustomArgs.(*conversionargs.CustomArgs); ok {
peerPkgs = append(peerPkgs, customArgs.BasePeerDirs...)
peerPkgs = append(peerPkgs, customArgs.ExtraPeerDirs...)
if len(peerPkgs) > 0 {
peerPkgs = append(peerPkgs, customArgs.BasePeerDirs...)
peerPkgs = append(peerPkgs, customArgs.ExtraPeerDirs...)
}
skipUnsafe = customArgs.SkipUnsafe
}
@ -457,12 +473,13 @@ type genConversion struct {
// the package that the conversion funcs are going to be output to
outputPackage string
// packages that contain the peer of types in typesPacakge
peerPackages []string
manualConversions conversionFuncMap
imports namer.ImportTracker
types []*types.Type
skippedFields map[*types.Type][]string
useUnsafe TypesEqual
peerPackages []string
manualConversions conversionFuncMap
imports namer.ImportTracker
types []*types.Type
explicitConversions []conversionPair
skippedFields map[*types.Type][]string
useUnsafe TypesEqual
}
func NewGenConversion(sanitizedName, typesPackage, outputPackage string, manualConversions conversionFuncMap, peerPkgs []string, useUnsafe TypesEqual) generator.Generator {
@ -470,14 +487,15 @@ func NewGenConversion(sanitizedName, typesPackage, outputPackage string, manualC
DefaultGen: generator.DefaultGen{
OptionalName: sanitizedName,
},
typesPackage: typesPackage,
outputPackage: outputPackage,
peerPackages: peerPkgs,
manualConversions: manualConversions,
imports: generator.NewImportTracker(),
types: []*types.Type{},
skippedFields: map[*types.Type][]string{},
useUnsafe: useUnsafe,
typesPackage: typesPackage,
outputPackage: outputPackage,
peerPackages: peerPkgs,
manualConversions: manualConversions,
imports: generator.NewImportTracker(),
types: []*types.Type{},
explicitConversions: []conversionPair{},
skippedFields: map[*types.Type][]string{},
useUnsafe: useUnsafe,
}
}
@ -534,17 +552,55 @@ func (g *genConversion) convertibleOnlyWithinPackage(inType, outType *types.Type
return true
}
func (g *genConversion) Filter(c *generator.Context, t *types.Type) bool {
peerType := getPeerTypeFor(c, t, g.peerPackages)
if peerType == nil {
return false
}
if !g.convertibleOnlyWithinPackage(t, peerType) {
return false
func getExplicitFromTypes(t *types.Type) []types.Name {
comments := append(t.SecondClosestCommentLines, t.CommentLines...)
paths := extractExplicitFromTag(comments)
result := []types.Name{}
for _, path := range paths {
items := strings.Split(path, ".")
if len(items) != 2 {
klog.Errorf("Unexpected k8s:conversion-gen:explicit-from tag: %s", path)
continue
}
switch {
case items[0] == "net/url" && items[1] == "Values":
default:
klog.Fatalf("Not supported k8s:conversion-gen:explicit-from tag: %s", path)
}
result = append(result, types.Name{Package: items[0], Name: items[1]})
}
return result
}
g.types = append(g.types, t)
return true
func (g *genConversion) Filter(c *generator.Context, t *types.Type) bool {
convertibleWithPeer := func() bool {
peerType := getPeerTypeFor(c, t, g.peerPackages)
if peerType == nil {
return false
}
if !g.convertibleOnlyWithinPackage(t, peerType) {
return false
}
g.types = append(g.types, t)
return true
}()
explicitlyConvertible := func() bool {
inTypes := getExplicitFromTypes(t)
if len(inTypes) == 0 {
return false
}
for i := range inTypes {
pair := conversionPair{
inType: &types.Type{Name: inTypes[i]},
outType: t,
}
g.explicitConversions = append(g.explicitConversions, pair)
}
return true
}()
return convertibleWithPeer || explicitlyConvertible
}
func (g *genConversion) isOtherPackage(pkg string) bool {
@ -618,6 +674,12 @@ func (g *genConversion) Init(c *generator.Context, w io.Writer) error {
args = argsFromType(peerType, t).With("Scope", types.Ref(conversionPackagePath, "Scope"))
sw.Do("if err := s.AddGeneratedConversionFunc((*$.inType|raw$)(nil), (*$.outType|raw$)(nil), func(a, b interface{}, scope $.Scope|raw$) error { return "+nameTmpl+"(a.(*$.inType|raw$), b.(*$.outType|raw$), scope) }); err != nil { return err }\n", args)
}
for i := range g.explicitConversions {
args := argsFromType(g.explicitConversions[i].inType, g.explicitConversions[i].outType).With("Scope", types.Ref(conversionPackagePath, "Scope"))
sw.Do("if err := s.AddGeneratedConversionFunc((*$.inType|raw$)(nil), (*$.outType|raw$)(nil), func(a, b interface{}, scope $.Scope|raw$) error { return "+nameTmpl+"(a.(*$.inType|raw$), b.(*$.outType|raw$), scope) }); err != nil { return err }\n", args)
}
var pairs []conversionPair
for pair, t := range g.manualConversions {
if t.Name.Package != g.outputPackage {
@ -644,10 +706,32 @@ func (g *genConversion) Init(c *generator.Context, w io.Writer) error {
func (g *genConversion) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
klog.V(5).Infof("generating for type %v", t)
peerType := getPeerTypeFor(c, t, g.peerPackages)
sw := generator.NewSnippetWriter(w, c, "$", "$")
g.generateConversion(t, peerType, sw)
g.generateConversion(peerType, t, sw)
if peerType := getPeerTypeFor(c, t, g.peerPackages); peerType != nil {
g.generateConversion(t, peerType, sw)
g.generateConversion(peerType, t, sw)
}
for _, inTypeName := range getExplicitFromTypes(t) {
inPkg, ok := c.Universe[inTypeName.Package]
if !ok {
klog.Errorf("Unrecognized package: %s", inTypeName.Package)
continue
}
inType, ok := inPkg.Types[inTypeName.Name]
if !ok {
klog.Errorf("Unrecognized type in package %s: %s", inTypeName.Package, inTypeName.Name)
continue
}
switch {
case inType.Name.Package == "net/url" && inType.Name.Name == "Values":
g.generateFromUrlValues(inType, t, sw)
default:
klog.Errorf("Not supported input type: %#v", inType.Name)
}
}
return sw.Error()
}
@ -972,6 +1056,124 @@ func (g *genConversion) doUnknown(inType, outType *types.Type, sw *generator.Sni
sw.Do("// FIXME: Type $.|raw$ is unsupported.\n", inType)
}
func (g *genConversion) generateFromUrlValues(inType, outType *types.Type, sw *generator.SnippetWriter) {
args := generator.Args{
"inType": inType,
"outType": outType,
"Scope": types.Ref(conversionPackagePath, "Scope"),
}
sw.Do("func auto"+nameTmpl+"(in *$.inType|raw$, out *$.outType|raw$, s $.Scope|raw$) error {\n", args)
for _, outMember := range outType.Members {
jsonTag := reflect.StructTag(outMember.Tags).Get("json")
index := strings.Index(jsonTag, ",")
if index == -1 {
index = len(jsonTag)
}
if index == 0 {
memberArgs := generator.Args{
"name": outMember.Name,
}
sw.Do("// WARNING: Field $.name$ does not have json tag, skipping.\n\n", memberArgs)
continue
}
memberArgs := generator.Args{
"name": outMember.Name,
"tag": jsonTag[:index],
}
sw.Do("if values, ok := map[string][]string(*in)[\"$.tag$\"]; ok && len(values) > 0 {\n", memberArgs)
g.fromValuesEntry(inType.Underlying.Elem, outMember, sw)
sw.Do("} else {\n", nil)
g.setZeroValue(outMember, sw)
sw.Do("}\n", nil)
}
sw.Do("return nil\n", nil)
sw.Do("}\n\n", nil)
if _, found := g.preexists(inType, outType); found {
// There is a public manual Conversion method: use it.
} else {
// Emit a public conversion function.
sw.Do("// "+nameTmpl+" is an autogenerated conversion function.\n", args)
sw.Do("func "+nameTmpl+"(in *$.inType|raw$, out *$.outType|raw$, s $.Scope|raw$) error {\n", args)
sw.Do("return auto"+nameTmpl+"(in, out, s)\n", args)
sw.Do("}\n\n", nil)
}
}
func (g *genConversion) fromValuesEntry(inType *types.Type, outMember types.Member, sw *generator.SnippetWriter) {
memberArgs := generator.Args{
"name": outMember.Name,
"type": outMember.Type,
}
if function, ok := g.preexists(inType, outMember.Type); ok {
args := memberArgs.With("function", function)
sw.Do("if err := $.function|raw$(&values, &out.$.name$, s); err != nil {\n", args)
sw.Do("return err\n", nil)
sw.Do("}\n", nil)
return
}
switch {
case outMember.Type == types.String:
sw.Do("out.$.name$ = values[0]\n", memberArgs)
case g.useUnsafe.Equal(inType, outMember.Type):
args := memberArgs.With("Pointer", types.Ref("unsafe", "Pointer"))
switch inType.Kind {
case types.Pointer:
sw.Do("out.$.name$ = ($.type|raw$)($.Pointer|raw$(&values))\n", args)
case types.Map, types.Slice:
sw.Do("out.$.name$ = *(*$.type|raw$)($.Pointer|raw$(&values))\n", args)
default:
// TODO: Support other types to allow more auto-conversions.
sw.Do("// FIXME: out.$.name$ is of not yet supported type and requires manual conversion\n", memberArgs)
}
default:
// TODO: Support other types to allow more auto-conversions.
sw.Do("// FIXME: out.$.name$ is of not yet supported type and requires manual conversion\n", memberArgs)
}
}
func (g *genConversion) setZeroValue(outMember types.Member, sw *generator.SnippetWriter) {
outMemberType := unwrapAlias(outMember.Type)
memberArgs := generator.Args{
"name": outMember.Name,
"alias": outMember.Type,
"type": outMemberType,
}
switch outMemberType.Kind {
case types.Builtin:
switch outMemberType {
case types.String:
sw.Do("out.$.name$ = \"\"\n", memberArgs)
case types.Int64, types.Int32, types.Int16, types.Int, types.Uint64, types.Uint32, types.Uint16, types.Uint:
sw.Do("out.$.name$ = 0\n", memberArgs)
case types.Uintptr, types.Byte:
sw.Do("out.$.name$ = 0\n", memberArgs)
case types.Float64, types.Float32, types.Float:
sw.Do("out.$.name$ = 0\n", memberArgs)
case types.Bool:
sw.Do("out.$.name$ = false\n", memberArgs)
default:
sw.Do("// FIXME: out.$.name$ is of unsupported type and requires manual conversion\n", memberArgs)
}
case types.Struct:
if outMemberType == outMember.Type {
sw.Do("out.$.name$ = $.type|raw${}\n", memberArgs)
} else {
sw.Do("out.$.name$ = $.alias|raw$($.type|raw${})\n", memberArgs)
}
case types.Map, types.Slice, types.Pointer:
sw.Do("out.$.name$ = nil\n", memberArgs)
case types.Alias:
// outMemberType was already unwrapped from aliases - so that should never happen.
sw.Do("// FIXME: unexpected error for out.$.name$\n", memberArgs)
case types.Interface, types.Array:
sw.Do("out.$.name$ = nil\n", memberArgs)
default:
sw.Do("// FIXME: out.$.name$ is of unsupported type and requires manual conversion\n", memberArgs)
}
}
func isDirectlyAssignable(inType, outType *types.Type) bool {
// TODO: This should maybe check for actual assignability between the two
// types, rather than superficial traits that happen to indicate it is

View File

@ -100,6 +100,15 @@ func main() {
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
// k8s.io/apimachinery/pkg/runtime contains a number of manual conversions,
// that we need to generate conversions.
// Packages being dependencies of explicitly requested packages are only
// partially scanned - only types explicitly used are being traversed.
// Not used functions or types are omitted.
// Adding this explicitly to InputDirs ensures that the package is fully
// scanned and all functions are parsed and processed.
genericArgs.InputDirs = append(genericArgs.InputDirs, "k8s.io/apimachinery/pkg/runtime")
if err := generatorargs.Validate(genericArgs); err != nil {
klog.Fatalf("Error: %v", err)
}