http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/builder/Builder.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/Builder.java b/src/main/groovy/groovy/transform/builder/Builder.java new file mode 100644 index 0000000..93b6090 --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/Builder.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static org.codehaus.groovy.transform.BuilderASTTransformation.BuilderStrategy; + +/** + * The {@code @Builder} AST transformation is used to help write classes that can be created using <em>fluent</em> api calls.<!-- --> + * The transform supports multiple building strategies to cover a range of cases and there are a number + * of configuration options to customize the building process. + * + * In addition, a number of annotation attributes let you customise the building process. Not all annotation attributes + * are supported by all strategies. See the individual strategy documentation for more details. + * If you're an AST hacker, you can also define your own strategy class. + * + * The following strategies are bundled with Groovy: + * <ul> + * <li>{@link SimpleStrategy} for creating chained setters</li> + * <li>{@link ExternalStrategy} where you annotate an explicit builder class while leaving some buildee class being built untouched</li> + * <li>{@link DefaultStrategy} which creates a nested helper class for instance creation</li> + * <li>{@link InitializerStrategy} which creates a nested helper class for instance creation which when used with {@code @CompileStatic} allows type-safe object creation</li> + * </ul> + * + * Note that Groovy provides other built-in mechanisms for easy creation of objects, e.g. the named-args constructor: + * <pre> + * new Person(firstName: "Robert", lastName: "Lewandowski", age: 21) + * </pre> + * or the with statement: + * <pre> + * new Person().with { + * firstName = "Robert" + * lastName = "Lewandowski" + * age = 21 + * } + * </pre> + * so you might not find value in using the builder transform at all. But if you need Java integration or in some cases improved type safety, the {@code @Builder} transform might prove very useful. + * + * @see groovy.transform.builder.SimpleStrategy + * @see groovy.transform.builder.ExternalStrategy + * @see groovy.transform.builder.DefaultStrategy + * @see groovy.transform.builder.InitializerStrategy + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.BuilderASTTransformation") +public @interface Builder { + + /** + * A class for which builder methods should be created. It will be an error to leave + * this attribute with its default value for some strategies. + */ + Class forClass() default Undefined.CLASS.class; + + /** + * A class capturing the builder strategy + */ + Class<? extends BuilderStrategy> builderStrategy() default DefaultStrategy.class; + + /** + * The prefix to use when creating the setter methods. + * Default is determined by the strategy which might use "" or "set" but you can choose your own, e.g. "with". + * If non-empty the first letter of the property will be capitalized before being appended to the prefix. + */ + String prefix() default Undefined.STRING; + + /** + * For strategies which create a builder helper class, the class name to use for the helper class. + * Not used if using {@code forClass} since in such cases the builder class is explicitly supplied. + * Default is determined by the strategy, e.g. <em>TargetClass</em> + "Builder" or <em>TargetClass</em> + "Initializer". + */ + String builderClassName() default Undefined.STRING; + + /** + * For strategies which create a builder helper class that creates the instance, the method name to call to create the instance. + * Default is determined by the strategy, e.g. <em>build</em> or <em>create</em>. + */ + String buildMethodName() default Undefined.STRING; + + /** + * The method name to use for a builder factory method in the source class for easy access of the + * builder helper class for strategies which create such a helper class. + * Must not be used if using {@code forClass}. + * Default is determined by the strategy, e.g. <em>builder</em> or <em>createInitializer</em>. + */ + String builderMethodName() default Undefined.STRING; + + /** + * List of field and/or property names to exclude from generated builder methods. + * Must not be used if 'includes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + */ + String[] excludes() default {}; + + /** + * List of field and/or property names to include within the generated builder methods. + * Must not be used if 'excludes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + * The default value is a special marker value indicating that no includes are defined; all fields + * are included if includes remains undefined and excludes is explicitly or implicitly an empty list. + */ + String[] includes() default {Undefined.STRING}; + + /** + * By default, 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. + */ + boolean useSetters() default false; + + /** + * Generate builder methods for properties from super classes. + */ + boolean includeSuperProperties() default false; + + /** + * Whether the generated builder should support all properties, including those with names that are considered internal. + * + * @since 2.5.0 + */ + boolean allNames() default false; + + /** + * Whether to include all properties (as per the JavaBean spec) in the generated builder. + * Groovy recognizes any field-like definitions with no explicit visibility as property definitions + * and always includes them in the {@code @Builder} generated classes. 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. + * Currently only supported by DefaultStrategy and ExternalStrategy. + * + * @since 2.5.0 + */ + boolean allProperties() default true; +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/builder/DefaultStrategy.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/DefaultStrategy.java b/src/main/groovy/groovy/transform/builder/DefaultStrategy.java new file mode 100644 index 0000000..65d90e3 --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/DefaultStrategy.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.InnerClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.transform.BuilderASTTransformation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE; +import static org.codehaus.groovy.ast.tools.GeneralUtils.args; +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.block; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; +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.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecRecurse; +import static org.codehaus.groovy.ast.tools.GenericsUtils.createGenericsSpec; +import static org.codehaus.groovy.ast.tools.GenericsUtils.extractSuperClassGenerics; +import static org.codehaus.groovy.ast.tools.GenericsUtils.newClass; +import static org.codehaus.groovy.transform.AbstractASTTransformation.getMemberStringValue; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_EXCEPTIONS; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_PARAMS; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; + +/** + * This strategy is used with the {@link Builder} AST transform to create a builder helper class + * for the fluent creation of instances of a specified class. It can be used at the class, + * static method or constructor levels. + * + * You use it as follows: + * <pre class="groovyTestCase"> + * import groovy.transform.builder.* + * + * {@code @Builder} + * class Person { + * String firstName + * String lastName + * int age + * } + * def person = Person.builder().firstName("Robert").lastName("Lewandowski").age(21).build() + * assert person.firstName == "Robert" + * assert person.lastName == "Lewandowski" + * assert person.age == 21 + * </pre> + * The {@code prefix} annotation attribute can be used to create setters with a different naming convention. The default is the + * empty string but you could change that to "set" as follows: + * <pre class="groovyTestCase"> + * {@code @groovy.transform.builder.Builder}(prefix='set') + * class Person { + * String firstName + * String lastName + * int age + * } + * def p2 = Person.builder().setFirstName("Robert").setLastName("Lewandowski").setAge(21).build() + * </pre> + * or using a prefix of 'with' would result in usage like this: + * <pre> + * def p3 = Person.builder().withFirstName("Robert").withLastName("Lewandowski").withAge(21).build() + * </pre> + * + * You can also use the {@code @Builder} annotation in combination with this strategy on one or more constructor or + * static method instead of or in addition to using it at the class level. An example with a constructor follows: + * <pre class="groovyTestCase"> + * import groovy.transform.ToString + * import groovy.transform.builder.Builder + * + * {@code @ToString} + * class Person { + * String first, last + * int born + * + * {@code @Builder} + * Person(String roleName) { + * if (roleName == 'Jack Sparrow') { + * first = 'Johnny'; last = 'Depp'; born = 1963 + * } + * } + * } + * assert Person.builder().roleName("Jack Sparrow").build().toString() == 'Person(Johnny, Depp, 1963)' + * </pre> + * In this case, the parameter(s) for the constructor or static method become the properties available + * in the builder. For the case of a static method, the return type of the static method becomes the + * class of the instance being created. For static factory methods, this is normally the class containing the + * static method but in general it can be any class. + * + * Note: if using more than one {@code @Builder} annotation, which is only possible when using static method + * or constructor variants, it is up to you to ensure that any generated helper classes or builder methods + * have unique names. E.g. we can modify the previous example to have three builders. At least two of the builders + * in our case will need to set the 'builderClassName' and 'builderMethodName' annotation attributes to ensure + * we have unique names. This is shown in the following example: + * <pre class="groovyTestCase"> + * import groovy.transform.builder.* + * import groovy.transform.* + * + * {@code @ToString} + * {@code @Builder} + * class Person { + * String first, last + * int born + * + * Person(){} // required to retain no-arg constructor + * + * {@code @Builder}(builderClassName='MovieBuilder', builderMethodName='byRoleBuilder') + * Person(String roleName) { + * if (roleName == 'Jack Sparrow') { + * this.first = 'Johnny'; this.last = 'Depp'; this.born = 1963 + * } + * } + * + * {@code @Builder}(builderClassName='SplitBuilder', builderMethodName='splitBuilder') + * static Person split(String name, int year) { + * def parts = name.split(' ') + * new Person(first: parts[0], last: parts[1], born: year) + * } + * } + * + * assert Person.splitBuilder().name("Johnny Depp").year(1963).build().toString() == 'Person(Johnny, Depp, 1963)' + * assert Person.byRoleBuilder().roleName("Jack Sparrow").build().toString() == 'Person(Johnny, Depp, 1963)' + * assert Person.builder().first("Johnny").last('Depp').born(1963).build().toString() == 'Person(Johnny, Depp, 1963)' + * </pre> + * + * The 'forClass' annotation attribute for the {@code @Builder} transform isn't applicable for this strategy. + * The 'useSetters' annotation attribute for the {@code @Builder} transform is ignored by this strategy which always uses setters. + */ +public class DefaultStrategy extends BuilderASTTransformation.AbstractBuilderStrategy { + private static final Expression DEFAULT_INITIAL_VALUE = null; + private static final int PUBLIC_STATIC = ACC_PUBLIC | ACC_STATIC; + + public void build(BuilderASTTransformation transform, AnnotatedNode annotatedNode, AnnotationNode anno) { + if (unsupportedAttribute(transform, anno, "forClass")) return; + if (annotatedNode instanceof ClassNode) { + buildClass(transform, (ClassNode) annotatedNode, anno); + } else if (annotatedNode instanceof MethodNode) { + buildMethod(transform, (MethodNode) annotatedNode, anno); + } + } + + public void buildMethod(BuilderASTTransformation transform, MethodNode mNode, AnnotationNode anno) { + if (transform.getMemberValue(anno, "includes") != null || transform.getMemberValue(anno, "excludes") != null) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + + " processing: includes/excludes only allowed on classes", anno); + } + ClassNode buildee = mNode.getDeclaringClass(); + ClassNode builder = createBuilder(anno, buildee); + createBuilderFactoryMethod(anno, buildee, builder); + for (Parameter parameter : mNode.getParameters()) { + builder.addField(createFieldCopy(buildee, parameter)); + builder.addMethod(createBuilderMethodForProp(builder, new PropertyInfo(parameter.getName(), parameter.getType()), getPrefix(anno))); + } + builder.addMethod(createBuildMethodForMethod(anno, buildee, mNode, mNode.getParameters())); + } + + public void buildClass(BuilderASTTransformation transform, ClassNode buildee, AnnotationNode anno) { + List<String> excludes = new ArrayList<String>(); + List<String> includes = new ArrayList<String>(); + includes.add(Undefined.STRING); + if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return; + if (includes.size() == 1 && Undefined.isUndefined(includes.get(0))) includes = null; + ClassNode builder = createBuilder(anno, buildee); + createBuilderFactoryMethod(anno, buildee, builder); +// List<FieldNode> fields = getFields(transform, anno, buildee); + boolean allNames = transform.memberHasValue(anno, "allNames", true); + boolean allProperties = !transform.memberHasValue(anno, "allProperties", false); + List<PropertyInfo> props = getPropertyInfos(transform, anno, buildee, excludes, includes, allNames, allProperties); + for (PropertyInfo pi : props) { + ClassNode correctedType = getCorrectedType(buildee, pi.getType(), builder); + String fieldName = pi.getName(); + builder.addField(createFieldCopy(buildee, fieldName, correctedType)); + builder.addMethod(createBuilderMethodForProp(builder, new PropertyInfo(fieldName, correctedType), getPrefix(anno))); + } + builder.addMethod(createBuildMethod(anno, buildee, props)); + } + + private static ClassNode getCorrectedType(ClassNode buildee, ClassNode fieldType, ClassNode declaringClass) { + Map<String,ClassNode> genericsSpec = createGenericsSpec(declaringClass); + extractSuperClassGenerics(fieldType, buildee, genericsSpec); + return correctToGenericsSpecRecurse(genericsSpec, fieldType); + } + + private static void createBuilderFactoryMethod(AnnotationNode anno, ClassNode buildee, ClassNode builder) { + buildee.getModule().addClass(builder); + buildee.addMethod(createBuilderMethod(anno, builder)); + } + + private static ClassNode createBuilder(AnnotationNode anno, ClassNode buildee) { + return new InnerClassNode(buildee, getFullName(anno, buildee), PUBLIC_STATIC, OBJECT_TYPE); + } + + private static String getFullName(AnnotationNode anno, ClassNode buildee) { + String builderClassName = getMemberStringValue(anno, "builderClassName", buildee.getNameWithoutPackage() + "Builder"); + return buildee.getName() + "$" + builderClassName; + } + + private static String getPrefix(AnnotationNode anno) { + return getMemberStringValue(anno, "prefix", ""); + } + + private static MethodNode createBuildMethodForMethod(AnnotationNode anno, ClassNode buildee, MethodNode mNode, Parameter[] params) { + String buildMethodName = getMemberStringValue(anno, "buildMethodName", "build"); + final BlockStatement body = new BlockStatement(); + ClassNode returnType; + if (mNode instanceof ConstructorNode) { + returnType = newClass(buildee); + body.addStatement(returnS(ctorX(newClass(mNode.getDeclaringClass()), args(params)))); + } else { + body.addStatement(returnS(callX(newClass(mNode.getDeclaringClass()), mNode.getName(), args(params)))); + returnType = newClass(mNode.getReturnType()); + } + return new MethodNode(buildMethodName, ACC_PUBLIC, returnType, NO_PARAMS, NO_EXCEPTIONS, body); + } + + private static MethodNode createBuilderMethod(AnnotationNode anno, ClassNode builder) { + String builderMethodName = getMemberStringValue(anno, "builderMethodName", "builder"); + final BlockStatement body = new BlockStatement(); + body.addStatement(returnS(ctorX(builder))); + return new MethodNode(builderMethodName, PUBLIC_STATIC, builder, NO_PARAMS, NO_EXCEPTIONS, body); + } + + private static MethodNode createBuildMethod(AnnotationNode anno, ClassNode buildee, List<PropertyInfo> props) { + String buildMethodName = getMemberStringValue(anno, "buildMethodName", "build"); + final BlockStatement body = new BlockStatement(); + body.addStatement(returnS(initializeInstance(buildee, props, body))); + return new MethodNode(buildMethodName, ACC_PUBLIC, newClass(buildee), NO_PARAMS, NO_EXCEPTIONS, body); + } + + private MethodNode createBuilderMethodForProp(ClassNode builder, PropertyInfo pinfo, String prefix) { + ClassNode fieldType = pinfo.getType(); + String fieldName = pinfo.getName(); + String setterName = getSetterName(prefix, fieldName); + return new MethodNode(setterName, ACC_PUBLIC, newClass(builder), params(param(fieldType, fieldName)), NO_EXCEPTIONS, block( + stmt(assignX(propX(varX("this"), constX(fieldName)), varX(fieldName, fieldType))), + returnS(varX("this", builder)) + )); + } + + private static FieldNode createFieldCopy(ClassNode buildee, Parameter param) { + Map<String,ClassNode> genericsSpec = createGenericsSpec(buildee); + extractSuperClassGenerics(param.getType(), buildee, genericsSpec); + ClassNode correctedParamType = correctToGenericsSpecRecurse(genericsSpec, param.getType()); + return new FieldNode(param.getName(), ACC_PRIVATE, correctedParamType, buildee, param.getInitialExpression()); + } + + private static FieldNode createFieldCopy(ClassNode buildee, String fieldName, ClassNode fieldType) { + return new FieldNode(fieldName, ACC_PRIVATE, fieldType, buildee, DEFAULT_INITIAL_VALUE); + } + + private static Expression initializeInstance(ClassNode buildee, List<PropertyInfo> props, BlockStatement body) { + Expression instance = varX("_the" + buildee.getNameWithoutPackage(), buildee); + body.addStatement(declS(instance, ctorX(buildee))); + for (PropertyInfo pi : props) { + body.addStatement(stmt(assignX(propX(instance, pi.getName()), varX(pi.getName(), pi.getType())))); + } + return instance; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/builder/ExternalStrategy.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/ExternalStrategy.java b/src/main/groovy/groovy/transform/builder/ExternalStrategy.java new file mode 100644 index 0000000..c482bef --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/ExternalStrategy.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.transform.BuilderASTTransformation; + +import java.util.ArrayList; +import java.util.List; + +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.block; +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.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.newClass; +import static org.codehaus.groovy.transform.BuilderASTTransformation.MY_TYPE_NAME; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_EXCEPTIONS; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_PARAMS; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; + +/** + * This strategy is used with the {@link Builder} AST transform to populate a builder helper class + * so that it can be used for the fluent creation of instances of a specified class. The specified class is not modified in any way and may be a Java class. + * + * You use it by creating and annotating an explicit builder class which will be filled in by during + * annotation processing with the appropriate build method and setters. An example is shown here: + * <pre class="groovyTestCase"> + * import groovy.transform.builder.* + * + * class Person { + * String firstName + * String lastName + * } + * + * {@code @Builder}(builderStrategy=ExternalStrategy, forClass=Person) + * class PersonBuilder { } + * + * def person = new PersonBuilder().firstName("Robert").lastName("Lewandowski").build() + * assert person.firstName == "Robert" + * assert person.lastName == "Lewandowski" + * </pre> + * The {@code prefix} annotation attribute, which defaults to the empty String for this strategy, can be used to create setters with a different naming convention, e.g. with + * the {@code prefix} changed to 'set', you would use your setters as follows: + * <pre> + * def p1 = new PersonBuilder().setFirstName("Robert").setLastName("Lewandowski").setAge(21).build() + * </pre> + * or using a prefix of 'with': + * <pre> + * def p2 = new PersonBuilder().withFirstName("Robert").withLastName("Lewandowski").withAge(21).build() + * </pre> + * + * The properties to use can be filtered using either the 'includes' or 'excludes' annotation attributes for {@code @Builder}. + * The {@code @Builder} 'buildMethodName' annotation attribute can be used for configuring the build method's name, default "build". + * + * The {@code @Builder} 'builderMethodName' and 'builderClassName' annotation attributes aren't applicable for this strategy. + * The {@code @Builder} 'useSetters' annotation attribute is ignored by this strategy which always uses setters. + */ +public class ExternalStrategy extends BuilderASTTransformation.AbstractBuilderStrategy { + private static final Expression DEFAULT_INITIAL_VALUE = null; + + public void build(BuilderASTTransformation transform, AnnotatedNode annotatedNode, AnnotationNode anno) { + if (!(annotatedNode instanceof ClassNode)) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + " processing: building for " + + annotatedNode.getClass().getSimpleName() + " not supported by " + getClass().getSimpleName(), annotatedNode); + return; + } + ClassNode builder = (ClassNode) annotatedNode; + String prefix = transform.getMemberStringValue(anno, "prefix", ""); + ClassNode buildee = transform.getMemberClassValue(anno, "forClass"); + if (buildee == null) { + transform.addError("Error during " + MY_TYPE_NAME + " processing: 'forClass' must be specified for " + getClass().getName(), anno); + return; + } + List<String> excludes = new ArrayList<String>(); + List<String> includes = new ArrayList<String>(); + includes.add(Undefined.STRING); + if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return; + if (includes.size() == 1 && Undefined.isUndefined(includes.get(0))) includes = null; + if (unsupportedAttribute(transform, anno, "builderClassName")) return; + if (unsupportedAttribute(transform, anno, "builderMethodName")) return; + boolean allNames = transform.memberHasValue(anno, "allNames", true); + boolean allProperties = !transform.memberHasValue(anno, "allProperties", false); + List<PropertyInfo> props = getPropertyInfos(transform, anno, buildee, excludes, includes, allNames, allProperties); + if (includes != null) { + for (String name : includes) { + checkKnownProperty(transform, anno, name, props); + } + } + for (PropertyInfo prop : props) { + builder.addField(createFieldCopy(builder, prop)); + builder.addMethod(createBuilderMethodForField(builder, prop, prefix)); + } + builder.addMethod(createBuildMethod(transform, anno, buildee, props)); + } + + private static MethodNode createBuildMethod(BuilderASTTransformation transform, AnnotationNode anno, ClassNode sourceClass, List<PropertyInfo> fields) { + String buildMethodName = transform.getMemberStringValue(anno, "buildMethodName", "build"); + final BlockStatement body = new BlockStatement(); + Expression sourceClassInstance = initializeInstance(sourceClass, fields, body); + body.addStatement(returnS(sourceClassInstance)); + return new MethodNode(buildMethodName, ACC_PUBLIC, sourceClass, NO_PARAMS, NO_EXCEPTIONS, body); + } + + private MethodNode createBuilderMethodForField(ClassNode builderClass, PropertyInfo prop, String prefix) { + String propName = prop.getName().equals("class") ? "clazz" : prop.getName(); + String setterName = getSetterName(prefix, prop.getName()); + return new MethodNode(setterName, ACC_PUBLIC, newClass(builderClass), params(param(newClass(prop.getType()), propName)), NO_EXCEPTIONS, block( + stmt(assignX(propX(varX("this"), constX(propName)), varX(propName))), + returnS(varX("this", newClass(builderClass))) + )); + } + + private static FieldNode createFieldCopy(ClassNode builderClass, PropertyInfo prop) { + String propName = prop.getName(); + return new FieldNode(propName.equals("class") ? "clazz" : propName, ACC_PRIVATE, newClass(prop.getType()), builderClass, DEFAULT_INITIAL_VALUE); + } + + private static Expression initializeInstance(ClassNode sourceClass, List<PropertyInfo> props, BlockStatement body) { + Expression instance = varX("_the" + sourceClass.getNameWithoutPackage(), sourceClass); + body.addStatement(declS(instance, ctorX(sourceClass))); + for (PropertyInfo prop : props) { + body.addStatement(stmt(assignX(propX(instance, prop.getName()), varX(prop.getName().equals("class") ? "clazz" : prop.getName(), newClass(prop.getType()))))); + } + return instance; + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/builder/InitializerStrategy.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/InitializerStrategy.java b/src/main/groovy/groovy/transform/builder/InitializerStrategy.java new file mode 100644 index 0000000..e59dac5 --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/InitializerStrategy.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.GenericsType; +import org.codehaus.groovy.ast.InnerClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.classgen.Verifier; +import org.codehaus.groovy.transform.AbstractASTTransformation; +import org.codehaus.groovy.transform.BuilderASTTransformation; +import org.codehaus.groovy.transform.ImmutableASTTransformation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE; +import static org.codehaus.groovy.ast.tools.GeneralUtils.args; +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.block; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorSuperS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorThisS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecRecurse; +import static org.codehaus.groovy.ast.tools.GenericsUtils.createGenericsSpec; +import static org.codehaus.groovy.ast.tools.GenericsUtils.extractSuperClassGenerics; +import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics; +import static org.codehaus.groovy.transform.AbstractASTTransformation.getMemberStringValue; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_EXCEPTIONS; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_PARAMS; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; + +/** + * This strategy is used with the {@link Builder} AST transform to create a builder helper class + * for the fluent and type-safe creation of instances of a specified class. + * + * It is modelled roughly on the design outlined here: + * http://michid.wordpress.com/2008/08/13/type-safe-builder-pattern-in-java/ + * + * You define classes which use the type-safe initializer pattern as follows: + * <pre> + * import groovy.transform.builder.* + * import groovy.transform.* + * + * {@code @ToString} + * {@code @Builder}(builderStrategy=InitializerStrategy) class Person { + * String firstName + * String lastName + * int age + * } + * </pre> + * While it isn't required to do so, the benefit of this builder strategy comes in conjunction with static type-checking or static compilation. Typical usage is as follows: + * <pre> + * {@code @CompileStatic} + * def main() { + * println new Person(Person.createInitializer().firstName("John").lastName("Smith").age(21)) + * } + * </pre> + * which prints: + * <pre> + * Person(John, Smith, 21) + * </pre> + * If you don't initialise some of the properties, your code won't compile, e.g. if the method body above was changed to this: + * <pre> + * println new Person(Person.createInitializer().firstName("John").lastName("Smith")) + * </pre> + * then the following compile-time error would result: + * <pre> + * [Static type checking] - Cannot find matching method Person#<init>(Person$PersonInitializer <groovy.transform.builder.InitializerStrategy$SET, groovy.transform.builder.InitializerStrategy$SET, groovy.transform.builder.InitializerStrategy$UNSET>). Please check if the declared type is correct and if the method exists. + * </pre> + * The message is a little cryptic, but it is basically the static compiler telling us that the third parameter, {@code age} in our case, is unset. + * + * You can also add this annotation to your predefined constructors. These will be made private and an initializer will be set up + * to call your constructor. Any parameters to your constructor become the properties expected by the initializer. + * If you use such a builder on a constructor as well as on the class or on more than one constructor, then it is up to you + * to define unique values for 'builderClassName' and 'builderMethodName' for each annotation. + */ +public class InitializerStrategy extends BuilderASTTransformation.AbstractBuilderStrategy { + + /** + * Internal phantom type used by the {@code InitializerStrategy} to indicate that a property has been set. It is used in conjunction with the generated parameterized type helper class. + */ + public abstract static class SET { + } + + /** + * Internal phantom type used by the {@code InitializerStrategy} to indicate that a property remains unset. It is used in conjunction with the generated parameterized type helper class. + */ + public abstract static class UNSET { + } + + private static final int PUBLIC_STATIC = ACC_PUBLIC | ACC_STATIC; + private static final Expression DEFAULT_INITIAL_VALUE = null; + + public void build(BuilderASTTransformation transform, AnnotatedNode annotatedNode, AnnotationNode anno) { + if (unsupportedAttribute(transform, anno, "forClass")) return; + if (unsupportedAttribute(transform, anno, "allProperties")) return; + boolean useSetters = transform.memberHasValue(anno, "useSetters", true); + boolean allNames = transform.memberHasValue(anno, "allNames", true); + if (annotatedNode instanceof ClassNode) { + createBuilderForAnnotatedClass(transform, (ClassNode) annotatedNode, anno, useSetters, allNames); + } else if (annotatedNode instanceof MethodNode) { + createBuilderForAnnotatedMethod(transform, (MethodNode) annotatedNode, anno, useSetters); + } + } + + private void createBuilderForAnnotatedClass(BuilderASTTransformation transform, ClassNode buildee, AnnotationNode anno, boolean useSetters, boolean allNames) { + List<String> excludes = new ArrayList<String>(); + List<String> includes = new ArrayList<String>(); + includes.add(Undefined.STRING); + if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return; + if (includes.size() == 1 && Undefined.isUndefined(includes.get(0))) includes = null; + List<FieldNode> fields = getFields(transform, anno, buildee); + List<FieldNode> filteredFields = filterFields(fields, includes, excludes, allNames); + if (filteredFields.isEmpty()) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + + " processing: at least one property is required for this strategy", anno); + } + ClassNode builder = createInnerHelperClass(buildee, getBuilderClassName(buildee, anno), filteredFields.size()); + addFields(buildee, filteredFields, builder); + + buildCommon(buildee, anno, filteredFields, builder); + createBuildeeConstructors(transform, buildee, builder, filteredFields, true, useSetters); + } + + private void createBuilderForAnnotatedMethod(BuilderASTTransformation transform, MethodNode mNode, AnnotationNode anno, boolean useSetters) { + if (transform.getMemberValue(anno, "includes") != null || transform.getMemberValue(anno, "excludes") != null) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + + " processing: includes/excludes only allowed on classes", anno); + } + if (mNode instanceof ConstructorNode) { + mNode.setModifiers(ACC_PRIVATE | ACC_SYNTHETIC); + } else { + if ((mNode.getModifiers() & ACC_STATIC) == 0) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + + " processing: method builders only allowed on static methods", anno); + } + mNode.setModifiers(ACC_PRIVATE | ACC_SYNTHETIC | ACC_STATIC); + } + ClassNode buildee = mNode.getDeclaringClass(); + Parameter[] parameters = mNode.getParameters(); + if (parameters.length == 0) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + + " processing: at least one parameter is required for this strategy", anno); + } + ClassNode builder = createInnerHelperClass(buildee, getBuilderClassName(buildee, anno), parameters.length); + List<FieldNode> convertedFields = convertParamsToFields(builder, parameters); + + buildCommon(buildee, anno, convertedFields, builder); + if (mNode instanceof ConstructorNode) { + createBuildeeConstructors(transform, buildee, builder, convertedFields, false, useSetters); + } else { + createBuildeeMethods(buildee, mNode, builder, convertedFields); + } + } + + private static String getBuilderClassName(ClassNode buildee, AnnotationNode anno) { + return getMemberStringValue(anno, "builderClassName", buildee.getNameWithoutPackage() + "Initializer"); + } + + private static void addFields(ClassNode buildee, List<FieldNode> filteredFields, ClassNode builder) { + for (FieldNode filteredField : filteredFields) { + builder.addField(createFieldCopy(buildee, filteredField)); + } + } + + private void buildCommon(ClassNode buildee, AnnotationNode anno, List<FieldNode> fieldNodes, ClassNode builder) { + String prefix = getMemberStringValue(anno, "prefix", ""); + String buildMethodName = getMemberStringValue(anno, "buildMethodName", "create"); + createBuilderConstructors(builder, buildee, fieldNodes); + buildee.getModule().addClass(builder); + String builderMethodName = getMemberStringValue(anno, "builderMethodName", "createInitializer"); + buildee.addMethod(createBuilderMethod(buildMethodName, builder, fieldNodes.size(), builderMethodName)); + for (int i = 0; i < fieldNodes.size(); i++) { + builder.addMethod(createBuilderMethodForField(builder, fieldNodes, prefix, i)); + } + builder.addMethod(createBuildMethod(builder, buildMethodName, fieldNodes)); + } + + private static List<FieldNode> convertParamsToFields(ClassNode builder, Parameter[] parameters) { + List<FieldNode> fieldNodes = new ArrayList<FieldNode>(); + for(Parameter parameter: parameters) { + Map<String,ClassNode> genericsSpec = createGenericsSpec(builder); + ClassNode correctedType = correctToGenericsSpecRecurse(genericsSpec, parameter.getType()); + FieldNode fieldNode = new FieldNode(parameter.getName(), parameter.getModifiers(), correctedType, builder, DEFAULT_INITIAL_VALUE); + fieldNodes.add(fieldNode); + builder.addField(fieldNode); + } + return fieldNodes; + } + + private static ClassNode createInnerHelperClass(ClassNode buildee, String builderClassName, int fieldsSize) { + final String fullName = buildee.getName() + "$" + builderClassName; + ClassNode builder = new InnerClassNode(buildee, fullName, PUBLIC_STATIC, OBJECT_TYPE); + GenericsType[] gtypes = new GenericsType[fieldsSize]; + for (int i = 0; i < gtypes.length; i++) { + gtypes[i] = makePlaceholder(i); + } + builder.setGenericsTypes(gtypes); + return builder; + } + + private static MethodNode createBuilderMethod(String buildMethodName, ClassNode builder, int numFields, String builderMethodName) { + final BlockStatement body = new BlockStatement(); + body.addStatement(returnS(callX(builder, buildMethodName))); + ClassNode returnType = makeClassSafeWithGenerics(builder, unsetGenTypes(numFields)); + return new MethodNode(builderMethodName, PUBLIC_STATIC, returnType, NO_PARAMS, NO_EXCEPTIONS, body); + } + + private static GenericsType[] unsetGenTypes(int numFields) { + GenericsType[] gtypes = new GenericsType[numFields]; + for (int i = 0; i < gtypes.length; i++) { + gtypes[i] = new GenericsType(ClassHelper.make(UNSET.class)); + } + return gtypes; + } + + private static GenericsType[] setGenTypes(int numFields) { + GenericsType[] gtypes = new GenericsType[numFields]; + for (int i = 0; i < gtypes.length; i++) { + gtypes[i] = new GenericsType(ClassHelper.make(SET.class)); + } + return gtypes; + } + + private static void createBuilderConstructors(ClassNode builder, ClassNode buildee, List<FieldNode> fields) { + builder.addConstructor(ACC_PRIVATE, NO_PARAMS, NO_EXCEPTIONS, block(ctorSuperS())); + final BlockStatement body = new BlockStatement(); + body.addStatement(ctorSuperS()); + initializeFields(fields, body, false); + builder.addConstructor(ACC_PRIVATE, getParams(fields, buildee), NO_EXCEPTIONS, body); + } + + private static void createBuildeeConstructors(BuilderASTTransformation transform, ClassNode buildee, ClassNode builder, List<FieldNode> fields, boolean needsConstructor, boolean useSetters) { + ConstructorNode initializer = createInitializerConstructor(buildee, builder, fields); + if (transform.hasAnnotation(buildee, ImmutableASTTransformation.MY_TYPE)) { + initializer.putNodeMetaData(ImmutableASTTransformation.IMMUTABLE_SAFE_FLAG, Boolean.TRUE); + } else if (needsConstructor) { + final BlockStatement body = new BlockStatement(); + body.addStatement(ctorSuperS()); + initializeFields(fields, body, useSetters); + buildee.addConstructor(ACC_PRIVATE | ACC_SYNTHETIC, getParams(fields, buildee), NO_EXCEPTIONS, body); + } + } + + private static void createBuildeeMethods(ClassNode buildee, MethodNode mNode, ClassNode builder, List<FieldNode> fields) { + ClassNode paramType = makeClassSafeWithGenerics(builder, setGenTypes(fields.size())); + List<Expression> argsList = new ArrayList<Expression>(); + Parameter initParam = param(paramType, "initializer"); + for (FieldNode fieldNode : fields) { + argsList.add(propX(varX(initParam), fieldNode.getName())); + } + String newName = "$" + mNode.getName(); // can't have private and public methods of the same name, so rename original + buildee.addMethod(mNode.getName(), PUBLIC_STATIC, mNode.getReturnType(), params(param(paramType, "initializer")), NO_EXCEPTIONS, + block(stmt(callX(buildee, newName, args(argsList))))); + renameMethod(buildee, mNode, newName); + } + + // no rename so delete and add + private static void renameMethod(ClassNode buildee, MethodNode mNode, String newName) { + buildee.addMethod(newName, mNode.getModifiers(), mNode.getReturnType(), mNode.getParameters(), mNode.getExceptions(), mNode.getCode()); + buildee.removeMethod(mNode); + } + + private static Parameter[] getParams(List<FieldNode> fields, ClassNode cNode) { + Parameter[] parameters = new Parameter[fields.size()]; + for (int i = 0; i < parameters.length; i++) { + FieldNode fNode = fields.get(i); + Map<String,ClassNode> genericsSpec = createGenericsSpec(fNode.getDeclaringClass()); + extractSuperClassGenerics(fNode.getType(), cNode, genericsSpec); + ClassNode correctedType = correctToGenericsSpecRecurse(genericsSpec, fNode.getType()); + parameters[i] = new Parameter(correctedType, fNode.getName()); + } + return parameters; + } + + private static ConstructorNode createInitializerConstructor(ClassNode buildee, ClassNode builder, List<FieldNode> fields) { + ClassNode paramType = makeClassSafeWithGenerics(builder, setGenTypes(fields.size())); + List<Expression> argsList = new ArrayList<Expression>(); + Parameter initParam = param(paramType, "initializer"); + for (FieldNode fieldNode : fields) { + argsList.add(propX(varX(initParam), fieldNode.getName())); + } + return buildee.addConstructor(ACC_PUBLIC, params(param(paramType, "initializer")), NO_EXCEPTIONS, block(ctorThisS(args(argsList)))); + } + + private static MethodNode createBuildMethod(ClassNode builder, String buildMethodName, List<FieldNode> fields) { + ClassNode returnType = makeClassSafeWithGenerics(builder, unsetGenTypes(fields.size())); + return new MethodNode(buildMethodName, PUBLIC_STATIC, returnType, NO_PARAMS, NO_EXCEPTIONS, block(returnS(ctorX(returnType)))); + } + + private MethodNode createBuilderMethodForField(ClassNode builder, List<FieldNode> fields, String prefix, int fieldPos) { + String fieldName = fields.get(fieldPos).getName(); + String setterName = getSetterName(prefix, fieldName); + GenericsType[] gtypes = new GenericsType[fields.size()]; + List<Expression> argList = new ArrayList<Expression>(); + for (int i = 0; i < fields.size(); i++) { + gtypes[i] = i == fieldPos ? new GenericsType(ClassHelper.make(SET.class)) : makePlaceholder(i); + argList.add(i == fieldPos ? propX(varX("this"), constX(fieldName)) : varX(fields.get(i).getName())); + } + ClassNode returnType = makeClassSafeWithGenerics(builder, gtypes); + FieldNode fNode = fields.get(fieldPos); + Map<String,ClassNode> genericsSpec = createGenericsSpec(fNode.getDeclaringClass()); + extractSuperClassGenerics(fNode.getType(), builder, genericsSpec); + ClassNode correctedType = correctToGenericsSpecRecurse(genericsSpec, fNode.getType()); + return new MethodNode(setterName, ACC_PUBLIC, returnType, params(param(correctedType, fieldName)), NO_EXCEPTIONS, block( + stmt(assignX(propX(varX("this"), constX(fieldName)), varX(fieldName, correctedType))), + returnS(ctorX(returnType, args(argList))) + )); + } + + private static GenericsType makePlaceholder(int i) { + ClassNode type = ClassHelper.makeWithoutCaching("T" + i); + type.setRedirect(OBJECT_TYPE); + type.setGenericsPlaceHolder(true); + return new GenericsType(type); + } + + private static FieldNode createFieldCopy(ClassNode buildee, FieldNode fNode) { + Map<String,ClassNode> genericsSpec = createGenericsSpec(fNode.getDeclaringClass()); + extractSuperClassGenerics(fNode.getType(), buildee, genericsSpec); + ClassNode correctedType = correctToGenericsSpecRecurse(genericsSpec, fNode.getType()); + return new FieldNode(fNode.getName(), fNode.getModifiers(), correctedType, buildee, DEFAULT_INITIAL_VALUE); + } + + private static List<FieldNode> filterFields(List<FieldNode> fieldNodes, List<String> includes, List<String> excludes, boolean allNames) { + List<FieldNode> fields = new ArrayList<FieldNode>(); + for (FieldNode fNode : fieldNodes) { + if (AbstractASTTransformation.shouldSkipUndefinedAware(fNode.getName(), excludes, includes, allNames)) continue; + fields.add(fNode); + } + return fields; + } + + private static void initializeFields(List<FieldNode> fields, BlockStatement body, boolean useSetters) { + for (FieldNode field : fields) { + String name = field.getName(); + body.addStatement( + stmt(useSetters && !field.isFinal() + ? callThisX(getSetterName(name), varX(param(field.getType(), name))) + : assignX(propX(varX("this"), field.getName()), varX(param(field.getType(), name))) + ) + ); + } + } + + private static String getSetterName(String name) { + return "set" + Verifier.capitalize(name); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/builder/SimpleStrategy.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/SimpleStrategy.java b/src/main/groovy/groovy/transform/builder/SimpleStrategy.java new file mode 100644 index 0000000..7956ac6 --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/SimpleStrategy.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.transform.AbstractASTTransformation; +import org.codehaus.groovy.transform.BuilderASTTransformation; +import org.objectweb.asm.Opcodes; + +import java.util.ArrayList; +import java.util.List; + +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.block; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.fieldX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstancePropertyFields; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.newClass; +import static org.codehaus.groovy.transform.AbstractASTTransformation.getMemberStringValue; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_EXCEPTIONS; + +/** + * This strategy is used with the {@link Builder} AST transform to modify your Groovy objects so that the + * setter methods for properties return the original object, thus allowing chained usage of the setters. + * + * You use it as follows: + * <pre class="groovyTestCase"> + * import groovy.transform.builder.* + * + * {@code @Builder}(builderStrategy=SimpleStrategy) + * class Person { + * String firstName + * String lastName + * int age + * } + * def person = new Person().setFirstName("Robert").setLastName("Lewandowski").setAge(21) + * assert person.firstName == "Robert" + * assert person.lastName == "Lewandowski" + * assert person.age == 21 + * </pre> + * The {@code prefix} annotation attribute can be used to create setters with a different naming convention, e.g. with the prefix set to the empty String, you would use your setters as follows: + * <pre> + * def p1 = new Person().firstName("Robert").lastName("Lewandowski").age(21) + * </pre> + * or using a prefix of 'with': + * <pre> + * def p2 = new Person().withFirstName("Robert").withLastName("Lewandowski").withAge(21) + * </pre> + * When using the default prefix of "set", Groovy's normal setters will be replaced by the chained versions. When using + * a custom prefix, Groovy's unchained setters will still be available for use in the normal unchained fashion. + * + * The 'useSetters' annotation attribute can be used for writable properties as per the {@code Builder} transform documentation. + * The other annotation attributes for the {@code @Builder} transform for configuring the building process aren't applicable for this strategy. + * + * @author Paul King + */ +public class SimpleStrategy extends BuilderASTTransformation.AbstractBuilderStrategy { + public void build(BuilderASTTransformation transform, AnnotatedNode annotatedNode, AnnotationNode anno) { + if (!(annotatedNode instanceof ClassNode)) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + " processing: building for " + + annotatedNode.getClass().getSimpleName() + " not supported by " + getClass().getSimpleName(), annotatedNode); + return; + } + ClassNode buildee = (ClassNode) annotatedNode; + if (unsupportedAttribute(transform, anno, "builderClassName")) return; + if (unsupportedAttribute(transform, anno, "buildMethodName")) return; + if (unsupportedAttribute(transform, anno, "builderMethodName")) return; + if (unsupportedAttribute(transform, anno, "forClass")) return; + if (unsupportedAttribute(transform, anno, "includeSuperProperties")) return; + if (unsupportedAttribute(transform, anno, "allProperties")) return; + boolean useSetters = transform.memberHasValue(anno, "useSetters", true); + boolean allNames = transform.memberHasValue(anno, "allNames", true); + + List<String> excludes = new ArrayList<String>(); + List<String> includes = new ArrayList<String>(); + includes.add(Undefined.STRING); + if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return; + if (includes.size() == 1 && Undefined.isUndefined(includes.get(0))) includes = null; + String prefix = getMemberStringValue(anno, "prefix", "set"); + List<FieldNode> fields = getFields(transform, anno, buildee); + if (includes != null) { + for (String name : includes) { + checkKnownField(transform, anno, name, fields); + } + } + for (FieldNode field : fields) { + String fieldName = field.getName(); + if (!AbstractASTTransformation.shouldSkipUndefinedAware(fieldName, excludes, includes, allNames)) { + String methodName = getSetterName(prefix, fieldName); + Parameter parameter = param(field.getType(), fieldName); + buildee.addMethod(methodName, Opcodes.ACC_PUBLIC, newClass(buildee), params(parameter), NO_EXCEPTIONS, block( + stmt(useSetters && !field.isFinal() + ? callThisX(getSetterName("set", fieldName), varX(parameter)) + : assignX(fieldX(field), varX(parameter)) + ), + returnS(varX("this"))) + ); + } + } + } + + @Override + protected List<FieldNode> getFields(BuilderASTTransformation transform, AnnotationNode anno, ClassNode buildee) { + return getInstancePropertyFields(buildee); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/stc/ClosureParams.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/ClosureParams.java b/src/main/groovy/groovy/transform/stc/ClosureParams.java new file mode 100644 index 0000000..e788d44 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/ClosureParams.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.stc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameter annotation aimed at helping IDEs or the static type checker to infer the + * parameter types of a closure. Without this annotation, a method signature may look like + * this:<p> + * <code>public <T,R> List<R> doSomething(List<T> source, Closure<R> consumer)</code> + * <p> + * <p>The problem this annotation tries to solve is to define the expected parameter types of the + * <i>consumer</i> closure. The generics type defined in <code>Closure<R></code> correspond to the + * result type of the closure, but tell nothing about what the closure must accept as arguments.</p> + * <p></p> + * <p>There's no way in Java or Groovy to express the type signature of the expected closure call method from + * outside the closure itself, so we rely on an annotation here. Unfortunately, annotations also have limitations + * (like not being able to use generics placeholder as annotation values) that prevent us from expressing the + * type directly.</p> + * <p>Additionally, closures are polymorphic. This means that a single closure can be used with different, valid, + * parameter signatures. A typical use case can be found when a closure accepts either a {@link java.util.Map.Entry} + * or a (key,value) pair, like the {@link org.codehaus.groovy.runtime.DefaultGroovyMethods#each(java.util.Map, groovy.lang.Closure)} + * method.</p> + * <p>For those reasons, the {@link ClosureParams} annotation takes these arguments: + * <ul> + * <li>{@link ClosureParams#value()} defines a {@link groovy.transform.stc.ClosureSignatureHint} hint class + * that the compiler will use to infer the parameter types</li> + * <li>{@link ClosureParams#conflictResolutionStrategy()} defines a {@link groovy.transform.stc.ClosureSignatureConflictResolver} resolver + * class that the compiler will use to potentially reduce ambiguities remaining after initial inference calculations</li> + * <li>{@link ClosureParams#options()}, a set of options that are passed to the hint when the type is inferred (and also available to the resolver)</li> + * </ul> + * </p> + * <p>As a result, the previous signature can be written like this:</p> + * <code>public <T,R> List<R> doSomething(List<T> source, @ClosureParams(FirstParam.FirstGenericType.class) Closure<R> consumer)</code> + * <p>Which uses the {@link FirstParam.FirstGenericType} first generic type of the first argument</p> hint to tell that the only expected + * argument type corresponds to the type of the first generic argument type of the first method parameter. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface ClosureParams { + Class<? extends ClosureSignatureHint> value(); + Class<? extends ClosureSignatureConflictResolver> conflictResolutionStrategy() default ClosureSignatureConflictResolver.class; + String[] options() default {}; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/stc/ClosureSignatureConflictResolver.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/ClosureSignatureConflictResolver.java b/src/main/groovy/groovy/transform/stc/ClosureSignatureConflictResolver.java new file mode 100644 index 0000000..d727958 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/ClosureSignatureConflictResolver.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.stc; + +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.List; + +/** + * If multiple candidate signatures are found after applying type hints, + * a conflict resolver can attempt to resolve the ambiguity. + * + * @since 2.5.0 + */ +public class ClosureSignatureConflictResolver { + /** + * + * @param candidates the list of signatures as determined after applying type hints and performing initial inference calculations + * @param receiver the receiver the method is being called on + * @param arguments the arguments for the closure + * @param closure the closure expression under analysis + * @param methodNode the method for which a {@link groovy.lang.Closure} parameter was annotated with {@link ClosureParams} + * @param sourceUnit the source unit of the file being compiled + * @param compilationUnit the compilation unit of the file being compiled + * @param options the options, corresponding to the {@link ClosureParams#options()} found on the annotation + * @return a non-null list of signatures, where a signature corresponds to an array of class nodes, each of them matching a parameter. A list with more than one element indicates that all ambiguities haven't yet been resolved. + */ + public List<ClassNode[]> resolve(List<ClassNode[]> candidates, ClassNode receiver, Expression arguments, ClosureExpression closure, + MethodNode methodNode, SourceUnit sourceUnit, CompilationUnit compilationUnit, String[] options) { + // do nothing by default + return candidates; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/stc/ClosureSignatureHint.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/ClosureSignatureHint.java b/src/main/groovy/groovy/transform/stc/ClosureSignatureHint.java new file mode 100644 index 0000000..9a77d20 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/ClosureSignatureHint.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.stc; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.GenericsType; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.List; + +/** + * <p>A closure signature hint class is always used in conjunction with the {@link ClosureParams} annotation. It is + * called at compile time (or may be used by IDEs) to infer the types of the parameters of a {@link groovy.lang.Closure}.</p> + + * <p>A closure hint class is responsible for generating the list of arguments that a closure accepts. Since closures + * may accept several signatures, {@link #getClosureSignatures(org.codehaus.groovy.ast.MethodNode, org.codehaus.groovy.control.SourceUnit, org.codehaus.groovy.control.CompilationUnit, String[], org.codehaus.groovy.ast.ASTNode)} should + * return a list.</p> + * + * <p>Whenever the type checker encounters a method call that targets a method accepting a closure, it will search + * for the {@link ClosureParams} annotation on the {@link groovy.lang.Closure} argument. If it is found, then it + * creates an instance of the hint class and calls the {@link #getClosureSignatures(org.codehaus.groovy.ast.MethodNode, org.codehaus.groovy.control.SourceUnit, org.codehaus.groovy.control.CompilationUnit, String[], org.codehaus.groovy.ast.ASTNode)} + * method, which will in turn return the list of signatures.</p> + * + * <p><i>Note that the signature concept here is used only to describe the parameter types, not the result type, which + * is found in the generic type argument of the {@link groovy.lang.Closure} class.</i></p> + * + * <p>Several predefined hints can be found, which should cover most of the use cases.</p> + * + * @author Cédric Champeau + * @since 2.3.0 + * + */ +public abstract class ClosureSignatureHint { + + /** + * A helper method which will extract the n-th generic type from a class node. + * @param type the class node from which to pick a generic type + * @param gtIndex the index of the generic type to extract + * @return the n-th generic type, or {@link org.codehaus.groovy.ast.ClassHelper#OBJECT_TYPE} if it doesn't exist. + */ + public static ClassNode pickGenericType(ClassNode type, int gtIndex) { + final GenericsType[] genericsTypes = type.getGenericsTypes(); + if (genericsTypes==null || genericsTypes.length<gtIndex) { + return ClassHelper.OBJECT_TYPE; + } + return genericsTypes[gtIndex].getType(); + } + + /** + * A helper method which will extract the n-th generic type from the n-th parameter of a method node. + * @param node the method node from which the generic type should be picked + * @param parameterIndex the index of the parameter in the method parameter list + * @param gtIndex the index of the generic type to extract + * @return the generic type, or {@link org.codehaus.groovy.ast.ClassHelper#OBJECT_TYPE} if it doesn't exist. + */ + public static ClassNode pickGenericType(MethodNode node, int parameterIndex, int gtIndex) { + final Parameter[] parameters = node.getParameters(); + final ClassNode type = parameters[parameterIndex].getOriginType(); + return pickGenericType(type, gtIndex); + } + + /** + * <p>Subclasses should implement this method, which returns the list of accepted closure signatures.</p> + * + * <p>The compiler will call this method each time, in a source file, a method call using a closure + * literal is encountered and that the target method has the corresponding {@link groovy.lang.Closure} parameter + * annotated with {@link groovy.transform.stc.ClosureParams}. So imagine the following code needs to be compiled:</p> + * + * <code>@groovy.transform.TypeChecked + * void doSomething() { + * println ['a','b'].collect { it.toUpperCase() } + * }</code> + * + * <p>The <i>collect</i> method accepts a closure, but normally, the type checker doesn't have enough type information + * in the sole {@link org.codehaus.groovy.runtime.DefaultGroovyMethods#collect(java.util.Collection, groovy.lang.Closure)} method + * signature to infer the type of <i>it</i>. With the annotation, it will now try to find an annotation on the closure parameter. + * If it finds it, then an instance of the hint class is created and the type checker calls it with the following arguments:</p> + * <ul> + * <li>the method node corresponding to the target method (here, the {@link org.codehaus.groovy.runtime.DefaultGroovyMethods#collect(java.util.Collection, groovy.lang.Closure)} method</li> + * <li>the (optional) list of options found in the annotation</li> + * </ul> + * + * <p>Now, the hint instance can return the list of expected parameters. Here, it would have to say that the collect method accepts + * a closure for which the only argument is of the type of the first generic type of the first argument.</p> + * <p>With that type information, the type checker can now infer that the type of <i>it</i> is <i>String</i>, because the first argument (here the receiver of the collect method) + * is a <i>List<String></i></p> + * + * <p></p> + * + * <p>Subclasses are therefore expected to return the signatures according to the available context, which is only the target method and the potential options.</p> + * + * + * @param node the method node for which a {@link groovy.lang.Closure} parameter was annotated with + * {@link ClosureParams} + * @param sourceUnit the source unit of the file being compiled + * @param compilationUnit the compilation unit of the file being compiled + * @param options the options, corresponding to the {@link ClosureParams#options()} found on the annotation @return a non-null list of signature, where a signature corresponds to an array of class nodes, each of them matching a parameter. + * @param usage the AST node, in the compiled file, which triggered a call to this method. Normally only used for logging/error handling + */ + public abstract List<ClassNode[]> getClosureSignatures(MethodNode node, SourceUnit sourceUnit, CompilationUnit compilationUnit, String[] options, ASTNode usage); + + /** + * Finds a class node given a string representing the type. Performs a lookup in the compilation unit to check if it is done in the same source unit. + * @param sourceUnit source unit + * @param compilationUnit compilation unit + * @param className the name of the class we want to get a {@link org.codehaus.groovy.ast.ClassNode} for + * @return a ClassNode representing the type + */ + protected ClassNode findClassNode(final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final String className) { + if (className.endsWith("[]")) { + return findClassNode(sourceUnit, compilationUnit, className.substring(0, className.length() - 2)).makeArray(); + } + ClassNode cn = compilationUnit.getClassNode(className); + if (cn == null) { + try { + cn = ClassHelper.make(Class.forName(className, false, sourceUnit.getClassLoader())); + } catch (ClassNotFoundException e) { + cn = ClassHelper.make(className); + } + } + return cn; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/stc/FirstParam.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/FirstParam.java b/src/main/groovy/groovy/transform/stc/FirstParam.java new file mode 100644 index 0000000..81eb0e7 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/FirstParam.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.stc; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; + +/** + * <p>A hint used to instruct the type checker to pick the first parameter type. For example:</p> + * <code>public <T> def doWith(T src, @ClosureParams(FirstParam.class) Closure c) { c.call(src); }</code> + * + * <p>This class has several inner classes that also helps picking generic argument types instead of the parameter type.</p> + * + * @author Cédric Champeau + * @since 2.3.0 + */ +public class FirstParam extends PickAnyArgumentHint { + public FirstParam() { + super(0,-1); + } + + /** + * <p>A hint used to instruct the type checker to pick the first generic type of the first parameter type. For example:</p> + * <code>void <T> doWithElements(List<T> src, @ClosureParams(FirstParam.FirstGenericType.class) Closure c) { src.each { c.call(it) } }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class FirstGenericType extends PickAnyArgumentHint { + public FirstGenericType() { + super(0,0); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the second generic type of the first parameter type. For example:</p> + * <code>void <T,U> doWithElements(Tuple<T,U> src, @ClosureParams(FirstParam.SecondGenericType.class) Closure c) { src.each { c.call(it) } }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class SecondGenericType extends PickAnyArgumentHint { + public SecondGenericType() { + super(0,1); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the third generic type of the first parameter type. For example:</p> + * <code>void <T,U,V> doWithElements(Triple<T,U,V> src, @ClosureParams(FirstParam.ThirdGenericType.class) Closure c) { src.each { c.call(it) } }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class ThirdGenericType extends PickAnyArgumentHint { + public ThirdGenericType() { + super(0,2); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the type of the component of the first parameter type, which is therefore + * expected to be an array, like in this example:</p> + * <code>void <T> doWithArray(T[] array, @ClosureParams(FirstParam.Component.class) Closure c) { array.each { c.call(it)} }</code> + */ + public static class Component extends FirstParam { + @Override + public ClassNode[] getParameterTypes(final MethodNode node, final String[] options, final SourceUnit sourceUnit, final CompilationUnit unit, final ASTNode usage) { + final ClassNode[] parameterTypes = super.getParameterTypes(node, options, sourceUnit, unit, usage); + parameterTypes[0] = parameterTypes[0].getComponentType(); + return parameterTypes; + } + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/transform/stc/FromAbstractTypeMethods.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/FromAbstractTypeMethods.java b/src/main/groovy/groovy/transform/stc/FromAbstractTypeMethods.java new file mode 100644 index 0000000..e5125f1 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/FromAbstractTypeMethods.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.stc; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.trait.Traits; + +import java.util.LinkedList; +import java.util.List; + +/** + * This signature hint uses abstract methods from some type (abstract class or interface) in order + * to infer the expected parameter types. This is especially useful for closure parameter type + * inference when implicit closure coercion is in action. + * + * @author Cédric Champeau + * @since 2.3.0 + */ +public class FromAbstractTypeMethods extends ClosureSignatureHint { + @Override + public List<ClassNode[]> getClosureSignatures(final MethodNode node, final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final String[] options, final ASTNode usage) { + String className = options[0]; + ClassNode cn = findClassNode(sourceUnit, compilationUnit, className); + return extractSignaturesFromMethods(cn); + } + + private static List<ClassNode[]> extractSignaturesFromMethods(final ClassNode cn) { + List<MethodNode> methods = cn.getAllDeclaredMethods(); + List<ClassNode[]> signatures = new LinkedList<ClassNode[]>(); + for (MethodNode method : methods) { + if (!method.isSynthetic() && method.isAbstract()) { + extractParametersFromMethod(signatures, method); + } + } + return signatures; + } + + private static void extractParametersFromMethod(final List<ClassNode[]> signatures, final MethodNode method) { + if (Traits.hasDefaultImplementation(method)) return; + Parameter[] parameters = method.getParameters(); + ClassNode[] typeParams = new ClassNode[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + typeParams[i] = parameters[i].getOriginType(); + } + signatures.add(typeParams); + } +}
