diff --git a/builder/struct.go b/builder/struct.go index 319bc76d..c3eb1aee 100644 --- a/builder/struct.go +++ b/builder/struct.go @@ -89,7 +89,7 @@ func (s *Struct) Assign(gen Generator, ctx *MethodContext, assignTo *AssignTo, s return nil, err.Lift(lift...) } if shouldCheckAgainstZero(ctx, nextSource, targetFieldType, assignTo.Update, false) { - stmt = append(stmt, jen.If(nextID.Code.Clone().Op("!=").Add(xtype.ZeroValue(nextSource.T))).Block(fieldStmt...)) + stmt = append(stmt, jen.If(xtype.NotZeroValueCheck(nextID.Code, nextSource.T)).Block(fieldStmt...)) } else { stmt = append(stmt, fieldStmt...) } @@ -131,7 +131,7 @@ func (s *Struct) Assign(gen Generator, ctx *MethodContext, assignTo *AssignTo, s callStmt = append(callStmt, assignTo.Stmt.Clone().Dot(targetField.Name()).Op("=").Add(callReturnID.Code)) if shouldCheckAgainstZero(ctx, functionCallSourceType, targetFieldType, assignTo.Update, true) { - stmt = append(stmt, jen.If(functionCallSourceID.Code.Clone().Op("!=").Add(xtype.ZeroValue(functionCallSourceType.T))).Block(callStmt...)) + stmt = append(stmt, jen.If(xtype.NotZeroValueCheck(functionCallSourceID.Code, functionCallSourceType.T)).Block(callStmt...)) } else { stmt = append(stmt, callStmt...) } diff --git a/scenario/update_ignore_zero_value_noncomparable_struct.yml b/scenario/update_ignore_zero_value_noncomparable_struct.yml new file mode 100644 index 00000000..4f2a4f97 --- /dev/null +++ b/scenario/update_ignore_zero_value_noncomparable_struct.yml @@ -0,0 +1,60 @@ +input: + input.go: | + package execution + + // Inner is not comparable (it contains a slice), so `inner != Inner{}` + // does not compile. goverter must use reflect.ValueOf(...).IsZero() when + // skipping zero values for such a field (#227). + type Inner struct { + Items []string + } + + type Source struct { + Inner Inner + } + + type Target struct { + Inner Inner + } + + // goverter:converter + // goverter:default:update + // goverter:update:ignoreZeroValueField + type testConverter interface { + // goverter:default NewTarget + ToTarget(source Source) Target + } + + func NewTarget() Target { + return Target{} + } +success: + - generated/generated.go: | + // Code generated by github.com/jmattheis/goverter, DO NOT EDIT. + + package generated + + import ( + execution "github.com/jmattheis/goverter/execution" + "reflect" + ) + + type testConverterImpl struct{} + + func (c *testConverterImpl) ToTarget(source execution.Source) execution.Target { + executionTarget := execution.NewTarget() + if !reflect.ValueOf(source.Inner).IsZero() { + executionTarget.Inner = c.executionInnerToExecutionInner(source.Inner) + } + return executionTarget + } + func (c *testConverterImpl) executionInnerToExecutionInner(source execution.Inner) execution.Inner { + var executionInner execution.Inner + if source.Items != nil { + executionInner.Items = make([]string, len(source.Items)) + for i := 0; i < len(source.Items); i++ { + executionInner.Items[i] = source.Items[i] + } + } + return executionInner + } diff --git a/xtype/zero.go b/xtype/zero.go index d8016bf0..3a08de35 100644 --- a/xtype/zero.go +++ b/xtype/zero.go @@ -6,6 +6,35 @@ import ( "github.com/dave/jennifer/jen" ) +// NotZeroValueCheck returns an expression that is true when sourceCode does not +// hold the zero value of type t. +// +// For most types this is `sourceCode != ` (where is `nil` for +// pointers, slices, maps, channels, functions and interfaces). Some struct and +// array types are not comparable because they contain a slice, map or function, +// and Go rejects `!=` on them, producing uncompilable generated code (see +// #227). For those, fall back to `!reflect.ValueOf(sourceCode).IsZero()`, which +// works for any type. +func NotZeroValueCheck(sourceCode *jen.Statement, t types.Type) *jen.Statement { + if zeroValueIsNil(t) || types.Comparable(t) { + return sourceCode.Clone().Op("!=").Add(ZeroValue(t)) + } + return jen.Op("!").Add( + jen.Qual("reflect", "ValueOf").Call(sourceCode.Clone()).Dot("IsZero").Call(), + ) +} + +// zeroValueIsNil reports whether the zero value of t is nil, i.e. t can be +// compared against nil regardless of whether it is otherwise comparable +// (slices and maps are not comparable but their zero value is nil). +func zeroValueIsNil(t types.Type) bool { + switch t.Underlying().(type) { + case *types.Pointer, *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface: + return true + } + return false +} + func ZeroValue(t types.Type) *jen.Statement { switch cast := t.(type) { case *types.Basic: