GROOVY-8465: @ToString should support the includeSuperFields annotation attribute
Project: http://git-wip-us.apache.org/repos/asf/groovy/repo Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/e9d8dffc Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/e9d8dffc Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/e9d8dffc Branch: refs/heads/GROOVY_2_5_X Commit: e9d8dffca03c81502dd979df6c57915aa0d0ac4c Parents: 990c271 Author: paulk <pa...@asert.com.au> Authored: Wed Jan 31 22:35:56 2018 +1000 Committer: paulk <pa...@asert.com.au> Committed: Wed Jan 31 22:41:30 2018 +1000 ---------------------------------------------------------------------- .../groovy/groovy/transform/MapConstructor.java | 18 ++++--- src/main/groovy/groovy/transform/ToString.java | 26 ++++++++--- .../groovy/transform/TupleConstructor.java | 22 +++++---- .../codehaus/groovy/ast/tools/GeneralUtils.java | 25 ++++++---- .../MapConstructorASTTransformation.java | 4 +- .../transform/ToStringASTTransformation.java | 49 ++++++++++++-------- .../TupleConstructorASTTransformation.java | 4 +- .../transform/ToStringTransformTest.groovy | 4 +- 8 files changed, 95 insertions(+), 57 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/main/groovy/groovy/transform/MapConstructor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/MapConstructor.java b/src/main/groovy/groovy/transform/MapConstructor.java index d4d5657..eb1e069 100644 --- a/src/main/groovy/groovy/transform/MapConstructor.java +++ b/src/main/groovy/groovy/transform/MapConstructor.java @@ -27,7 +27,7 @@ import java.lang.annotation.Target; /** * Class annotation used to assist in the creation of map constructors in classes. - * If the class is also annotated with {@code @KnownImmutable}, then the generated + * If the class is also annotated with {@code @ImmutableBase}, then the generated * constructor will contain additional code needed for immutable classes. * <p> * It allows you to write classes in this shortened form: @@ -91,22 +91,26 @@ public @interface MapConstructor { String[] includes() default {Undefined.STRING}; /** - * Include fields in the constructor. + * Include properties in the constructor. */ - boolean includeFields() default false; + boolean includeProperties() default true; /** - * Include properties in the constructor. + * Include fields in the constructor. Fields come after any properties. */ - boolean includeProperties() default true; + boolean includeFields() default false; /** * Include properties from super classes in the constructor. + * Groovy properties, JavaBean properties and fields (in that order) from superclasses come before + * the members from a subclass (unless 'includes' is used to determine the order). */ boolean includeSuperProperties() default false; /** * Include fields from super classes in the constructor. + * Groovy properties, JavaBean properties and fields (in that order) from superclasses come before + * the members from a subclass (unless 'includes' is used to determine the order). */ boolean includeSuperFields() default false; @@ -114,11 +118,13 @@ public @interface MapConstructor { * Whether to include all properties (as per the JavaBean spec) in the generated constructor. * When true, Groovy treats any explicitly created setXxx() methods as property setters as per the JavaBean * specification. + * JavaBean properties come after any Groovy properties but before any fields for a given class + * (unless 'includes' is used to determine the order). */ boolean allProperties() default false; /** - * By default, properties are set directly using their respective field. + * By default, Groovy properties are set directly using their respective field. * By setting {@code useSetters=true} then a writable property will be set using its setter. * If turning on this flag we recommend that setters that might be called are * made null-safe wrt the parameter. http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/main/groovy/groovy/transform/ToString.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/ToString.java b/src/main/groovy/groovy/transform/ToString.java index 968e7f0..49930d2 100644 --- a/src/main/groovy/groovy/transform/ToString.java +++ b/src/main/groovy/groovy/transform/ToString.java @@ -283,22 +283,32 @@ public @interface ToString { boolean includeSuper() default false; /** - * Whether to include super properties in the generated toString. - * @since 2.4.0 - */ - boolean includeSuperProperties() default false; - - /** * Whether to include names of properties/fields in the generated toString. */ boolean includeNames() default false; /** - * Include fields as well as properties in the generated toString. + * Include fields as well as properties in the generated toString. Fields come after any properties. */ boolean includeFields() default false; /** + * Whether to include super properties in the generated toString. + * Groovy properties, JavaBean properties and fields (in that order) from superclasses come after + * the members from a subclass (unless 'includes' is used to determine the order). + * + * @since 2.4.0 + */ + boolean includeSuperProperties() default false; + + /** + * Include super fields in the generated toString. + * Groovy properties, JavaBean properties and fields (in that order) from superclasses come after + * the members from a subclass (unless 'includes' is used to determine the order). + */ + boolean includeSuperFields() default false; + + /** * Don't display any fields or properties with value <tt>null</tt>. */ boolean ignoreNulls() default false; @@ -317,6 +327,8 @@ public @interface ToString { * appropriate getters and setters). Groovy also treats any explicitly created getXxx() or isYyy() * methods as property getters as per the JavaBean specification. Old versions of Groovy did not. * So set this flag to false for the old behavior or if you want to explicitly exclude such properties. + * JavaBean properties come after any Groovy properties but before any fields for a given class + * (unless 'includes' is used to determine the order). * * @since 2.5 */ http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/main/groovy/groovy/transform/TupleConstructor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/TupleConstructor.java b/src/main/groovy/groovy/transform/TupleConstructor.java index eb7cb6e..cbc384d 100644 --- a/src/main/groovy/groovy/transform/TupleConstructor.java +++ b/src/main/groovy/groovy/transform/TupleConstructor.java @@ -27,7 +27,7 @@ import java.lang.annotation.Target; /** * Class annotation used to assist in the creation of tuple constructors in classes. - * If the class is also annotated with {@code @KnownImmutable}, then the generated + * If the class is also annotated with {@code @ImmutableBase}, then the generated * constructor will contain additional code needed for immutable classes. * * Should be used with care with other annotations which create constructors - see "Known @@ -198,26 +198,30 @@ public @interface TupleConstructor { String[] includes() default {Undefined.STRING}; /** - * Include fields in the constructor. - */ - boolean includeFields() default false; - - /** * Include properties in the constructor. */ boolean includeProperties() default true; /** - * Include fields from super classes in the constructor. + * Include fields in the constructor. Fields come after any properties. */ - boolean includeSuperFields() default false; + boolean includeFields() default false; /** * Include properties from super classes in the constructor. + * Groovy properties, JavaBean properties and fields (in that order) from superclasses come before + * the members from a subclass (unless 'includes' is used to determine the order). */ boolean includeSuperProperties() default false; /** + * Include fields from super classes in the constructor. + * Groovy properties, JavaBean properties and fields (in that order) from superclasses come before + * the members from a subclass (unless 'includes' is used to determine the order). + */ + boolean includeSuperFields() default false; + + /** * Should super properties be called within a call to the parent constructor * rather than set as properties. Typically used in combination with {@code includeSuperProperties}. * Can't be true if using {@code pre} with a {@code super} first statement. @@ -270,6 +274,8 @@ public @interface TupleConstructor { * Whether to include all properties (as per the JavaBean spec) in the generated constructor. * When true, Groovy treats any explicitly created setXxx() methods as property setters as per the JavaBean * specification. + * JavaBean properties come after any Groovy properties but before any fields for a given class + * (unless 'includes' is used to determine the order). * * @since 2.5.0 */ http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java index b81e2e8..86a604a 100644 --- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java +++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java @@ -464,16 +464,18 @@ public class GeneralUtils { return result; } - public static List<PropertyNode> getAllProperties(Set<String> names, ClassNode cNode, boolean includeProperties, boolean includeFields, boolean allProperties, boolean traverseSuperClasses, boolean skipReadonly) { - return getAllProperties(names, cNode, cNode, includeProperties, includeFields, allProperties, traverseSuperClasses, skipReadonly); + public static List<PropertyNode> getAllProperties(Set<String> names, ClassNode cNode, boolean includeProperties, boolean includeFields, boolean includePseudoGetters, boolean includePseudoSetters, boolean traverseSuperClasses, boolean skipReadonly) { + return getAllProperties(names, cNode, cNode, includeProperties, includeFields, includePseudoGetters, includePseudoSetters, traverseSuperClasses, skipReadonly); } - public static List<PropertyNode> getAllProperties(Set<String> names, ClassNode origType, ClassNode cNode, boolean includeProperties, boolean includeFields, boolean allProperties, boolean traverseSuperClasses, boolean skipReadonly) { - final List<PropertyNode> result; - if (cNode == ClassHelper.OBJECT_TYPE || !traverseSuperClasses) { - result = new ArrayList<PropertyNode>(); - } else { - result = getAllProperties(names, origType, cNode.getSuperClass(), includeProperties, includeFields, allProperties, true, skipReadonly); + public static List<PropertyNode> getAllProperties(Set<String> names, ClassNode origType, ClassNode cNode, boolean includeProperties, boolean includeFields, boolean includePseudoGetters, boolean includePseudoSetters, boolean traverseSuperClasses, boolean skipReadonly) { + return getAllProperties(names, origType, cNode, includeProperties, includeFields, includePseudoGetters, includePseudoSetters, traverseSuperClasses, skipReadonly, false); + } + + public static List<PropertyNode> getAllProperties(Set<String> names, ClassNode origType, ClassNode cNode, boolean includeProperties, boolean includeFields, boolean includePseudoGetters, boolean includePseudoSetters, boolean traverseSuperClasses, boolean skipReadonly, boolean reverse) { + final List<PropertyNode> result = new ArrayList<PropertyNode>(); + if (cNode != ClassHelper.OBJECT_TYPE && traverseSuperClasses && !reverse) { + result.addAll(getAllProperties(names, origType, cNode.getSuperClass(), includeProperties, includeFields, includePseudoGetters, includePseudoSetters, true, skipReadonly)); } if (includeProperties) { for (PropertyNode pNode : cNode.getProperties()) { @@ -482,8 +484,8 @@ public class GeneralUtils { names.add(pNode.getName()); } } - if (allProperties) { - BeanUtils.addPseudoProperties(origType, cNode, result, names, false, false, true); + if (includePseudoGetters || includePseudoSetters) { + BeanUtils.addPseudoProperties(origType, cNode, result, names, false, includePseudoGetters, includePseudoSetters); } } if (includeFields) { @@ -501,6 +503,9 @@ public class GeneralUtils { names.add(fNode.getName()); } } + if (cNode != ClassHelper.OBJECT_TYPE && traverseSuperClasses && reverse) { + result.addAll(getAllProperties(names, origType, cNode.getSuperClass(), includeProperties, includeFields, includePseudoGetters, includePseudoSetters, true, skipReadonly)); + } return result; } http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java index 5d828b2..468419f 100644 --- a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java @@ -136,11 +136,11 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation { Set<String> names = new HashSet<String>(); List<PropertyNode> superList; if (includeSuperProperties || includeSuperFields) { - superList = getAllProperties(names, cNode, cNode.getSuperClass(), includeSuperProperties, includeSuperFields, allProperties, true, true); + superList = getAllProperties(names, cNode, cNode.getSuperClass(), includeSuperProperties, includeSuperFields, false, allProperties, true, true); } else { superList = new ArrayList<PropertyNode>(); } - List<PropertyNode> list = getAllProperties(names, cNode, true, includeFields, allProperties, false, true); + List<PropertyNode> list = getAllProperties(names, cNode, true, includeFields, false, allProperties, false, true); boolean makeImmutable = ImmutableASTTransformation.makeImmutable(cNode); http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java index 470e4c8..fc7daa9 100644 --- a/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java @@ -44,7 +44,9 @@ import org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import static org.codehaus.groovy.ast.ClassHelper.make; import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS; @@ -54,6 +56,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; import static org.codehaus.groovy.ast.tools.GeneralUtils.declS; import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.getAllProperties; import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceNonPropertyFields; import static org.codehaus.groovy.ast.tools.GeneralUtils.getterThisX; import static org.codehaus.groovy.ast.tools.GeneralUtils.hasDeclaredMethod; @@ -88,6 +91,7 @@ public class ToStringASTTransformation extends AbstractASTTransformation { if (!checkNotInterface(cNode, MY_TYPE_NAME)) return; boolean includeSuper = memberHasValue(anno, "includeSuper", true); boolean includeSuperProperties = memberHasValue(anno, "includeSuperProperties", true); + boolean includeSuperFields = memberHasValue(anno, "includeSuperFields", true); boolean cacheToString = memberHasValue(anno, "cache", true); List<String> excludes = getMemberStringList(anno, "excludes"); List<String> includes = getMemberStringList(anno, "includes"); @@ -107,7 +111,7 @@ public class ToStringASTTransformation extends AbstractASTTransformation { if (!checkIncludeExcludeUndefinedAware(anno, excludes, includes, MY_TYPE_NAME)) return; if (!checkPropertyList(cNode, includes != null ? DefaultGroovyMethods.minus(includes, "super") : null, "includes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties)) return; if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties)) return; - createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cacheToString, includeSuperProperties, allProperties, allNames); + createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cacheToString, includeSuperProperties, allProperties, allNames, includeSuperFields); } } @@ -132,10 +136,10 @@ public class ToStringASTTransformation extends AbstractASTTransformation { } public static void createToString(ClassNode cNode, boolean includeSuper, boolean includeFields, List<String> excludes, List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean cache, boolean includeSuperProperties, boolean allProperties) { - createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cache, includeSuperProperties, allProperties, false); + createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cache, includeSuperProperties, allProperties, false, false); } - public static void createToString(ClassNode cNode, boolean includeSuper, boolean includeFields, List<String> excludes, List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean cache, boolean includeSuperProperties, boolean allProperties, boolean allNames) { + public static void createToString(ClassNode cNode, boolean includeSuper, boolean includeFields, List<String> excludes, List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean cache, boolean includeSuperProperties, boolean allProperties, boolean allNames, boolean includeSuperFields) { // make a public method if none exists otherwise try a private method with leading underscore boolean hasExistingToString = hasDeclaredMethod(cNode, "toString", 0); if (hasExistingToString && hasDeclaredMethod(cNode, "_toString", 0)) return; @@ -147,11 +151,11 @@ public class ToStringASTTransformation extends AbstractASTTransformation { final Expression savedToString = varX(cacheField); body.addStatement(ifS( equalsNullX(savedToString), - assignS(savedToString, calculateToStringStatements(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames)) + assignS(savedToString, calculateToStringStatements(cNode, includeSuper, includeFields, includeSuperFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames)) )); tempToString = savedToString; } else { - tempToString = calculateToStringStatements(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames); + tempToString = calculateToStringStatements(cNode, includeSuper, includeFields, includeSuperFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames); } body.addStatement(returnS(tempToString)); @@ -171,7 +175,7 @@ public class ToStringASTTransformation extends AbstractASTTransformation { boolean canBeSelf; } - private static Expression calculateToStringStatements(ClassNode cNode, boolean includeSuper, boolean includeFields, List<String> excludes, final List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean includeSuperProperties, boolean allProperties, BlockStatement body, boolean allNames) { + private static Expression calculateToStringStatements(ClassNode cNode, boolean includeSuper, boolean includeFields, boolean includeSuperFields, List<String> excludes, final List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean includeSuperProperties, boolean allProperties, BlockStatement body, boolean allNames) { // def _result = new StringBuilder() final Expression result = varX("_result"); body.addStatement(declS(result, ctorX(STRINGBUILDER_TYPE))); @@ -185,21 +189,26 @@ public class ToStringASTTransformation extends AbstractASTTransformation { String className = (includePackage) ? cNode.getName() : cNode.getNameWithoutPackage(); body.addStatement(appendS(result, constX(className + "("))); - // append properties - List<PropertyNode> pList = BeanUtils.getAllProperties(cNode, includeSuperProperties, false, allProperties); - for (PropertyNode pNode : pList) { - if (shouldSkip(pNode.getName(), excludes, includes, allNames)) continue; - Expression getter = getterThisX(cNode, pNode); - elements.add(new ToStringElement(getter, pNode.getName(), canBeSelf(cNode, pNode.getOriginType()))); + Set<String> names = new HashSet<String>(); + List<PropertyNode> superList; + if (includeSuperProperties || includeSuperFields) { + superList = getAllProperties(names, cNode, cNode.getSuperClass(), includeSuperProperties, includeSuperFields, allProperties, false, true, true, true); + } else { + superList = new ArrayList<PropertyNode>(); } - - // append fields if needed - if (includeFields) { - List<FieldNode> fList = new ArrayList<FieldNode>(); - fList.addAll(getInstanceNonPropertyFields(cNode)); - for (FieldNode fNode : fList) { - if (shouldSkip(fNode.getName(), excludes, includes, allNames)) continue; - elements.add(new ToStringElement(varX(fNode), fNode.getName(), canBeSelf(cNode, fNode.getType()))); + List<PropertyNode> list = getAllProperties(names, cNode, true, includeFields, allProperties, false, false, true); + list.addAll(superList); + + for (PropertyNode pNode : list) { + String name = pNode.getName(); + if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue; + FieldNode fNode = pNode.getField(); + if (!cNode.hasProperty(name) && fNode.getDeclaringClass() != null) { + // it's really just a field + elements.add(new ToStringElement(varX(fNode), name, canBeSelf(cNode, fNode.getType()))); + } else { + Expression getter = getterThisX(cNode, pNode); + elements.add(new ToStringElement(getter, name, canBeSelf(cNode, pNode.getType()))); } } http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java index dd26c28..2b7088a 100644 --- a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java @@ -180,12 +180,12 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation Set<String> names = new HashSet<String>(); List<PropertyNode> superList; if (includeSuperProperties || includeSuperFields) { - superList = getAllProperties(names, cNode.getSuperClass(), includeSuperProperties, includeSuperFields, allProperties, true, true); + superList = getAllProperties(names, cNode.getSuperClass(), includeSuperProperties, includeSuperFields, false, allProperties, true, true); } else { superList = new ArrayList<PropertyNode>(); } - List<PropertyNode> list = getAllProperties(names, cNode, true, includeFields, allProperties, false, true); + List<PropertyNode> list = getAllProperties(names, cNode, true, includeFields, false, allProperties, false, true); boolean makeImmutable = makeImmutable(cNode); boolean specialHashMapCase = (ImmutableASTTransformation.isSpecialHashMapCase(list) && superList.isEmpty()) || http://git-wip-us.apache.org/repos/asf/groovy/blob/e9d8dffc/src/test/org/codehaus/groovy/transform/ToStringTransformTest.groovy ---------------------------------------------------------------------- diff --git a/src/test/org/codehaus/groovy/transform/ToStringTransformTest.groovy b/src/test/org/codehaus/groovy/transform/ToStringTransformTest.groovy index 200ef14..1fa678f 100644 --- a/src/test/org/codehaus/groovy/transform/ToStringTransformTest.groovy +++ b/src/test/org/codehaus/groovy/transform/ToStringTransformTest.groovy @@ -267,7 +267,7 @@ class ToStringTransformTest extends GroovyShellTestCase { } new SportsPerson(first: 'John', last: 'Smith', title: 'Mr').toString() ''') - assert toString == "SportsPerson(title:Mr, golfer:false, adult:true, cyclist:true, full:John Smith, senior:false, born:1975)" + assert toString == "SportsPerson(title:Mr, cyclist:true, full:John Smith, golfer:false, senior:false, born:1975, adult:true)" // same again but with allProperties=false and with @CompileStatic for test coverage purposes toString = evaluate(''' import groovy.transform.* @@ -294,7 +294,7 @@ class ToStringTransformTest extends GroovyShellTestCase { } new SportsPerson(first: 'John', last: 'Smith', title: 'Mr').toString() ''') - assert toString == "SportsPerson(title:Mr, golfer:false, adult:true, cyclist:true)" + assert toString == "SportsPerson(title:Mr, adult:true, cyclist:true, golfer:false)" } void testSelfReference() {