http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/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..c678c38 --- /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 right 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/d638ca43/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/d638ca43/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/d638ca43/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/d638ca43/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/d638ca43/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/d638ca43/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); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/FromString.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/FromString.java b/src/main/groovy/groovy/transform/stc/FromString.java new file mode 100644 index 0000000..12f9371 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/FromString.java @@ -0,0 +1,80 @@ +/* + * 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.tools.GenericsUtils; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.ArrayList; +import java.util.List; + + +/** + * <p>A closure parameter hint class that is convenient if you want to use a String representation + * of the signature. It makes use of the {@link ClosureParams#options() option strings}, where + * each string corresponds to a single signature.</p> + * + * <p>The following example describes a closure as accepting a single signature (List<T> list ->):</p> + * + * <code>public <T> T apply(T src, @ClosureParams(value=FromString.class, options="List<T>" Closure<T> cl)</code> + * + * <p>The next example describes a closure as accepting two signatures (List<T> list ->) and (T t ->):</p> + * + * <code>public <T> T apply(T src, @ClosureParams(value=FromString.class, options={"List<T>","T"} Closure<T> cl)</code> + * + * <p>It is advisable not to use this hint as a replacement for the various {@link FirstParam}, {@link SimpleType}, + * ... hints because it is actually much slower. Using this hint should therefore be limited + * to cases where it's not possible to express the signature using the existing hints.</p> + * + * @author Cédric Champeau + * @since 2.3.0 + */ +public class FromString extends ClosureSignatureHint { + + @Override + public List<ClassNode[]> getClosureSignatures(final MethodNode node, final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final String[] options, final ASTNode usage) { + List<ClassNode[]> list = new ArrayList<ClassNode[]>(options.length); + for (String option : options) { + list.add(parseOption(option, sourceUnit, compilationUnit, node, usage)); + } + return list; + } + + /** + * Parses a string representing a type, that must be aligned with the current context. + * For example, <i>"List<T>"</i> must be converted into the appropriate ClassNode + * for which <i>T</i> matches the appropriate placeholder. + * + * + * @param option a string representing a type + * @param sourceUnit the source unit (of the file being compiled) + * @param compilationUnit the compilation unit (of the file being compiled) + * @param mn the method node + * @param usage + * @return a class node if it could be parsed and resolved, null otherwise + */ + private static ClassNode[] parseOption(final String option, final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final MethodNode mn, final ASTNode usage) { + return GenericsUtils.parseClassNodesFromString(option, sourceUnit, compilationUnit, mn, usage); + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/IncorrectTypeHintException.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/IncorrectTypeHintException.java b/src/main/groovy/groovy/transform/stc/IncorrectTypeHintException.java new file mode 100644 index 0000000..aed167a --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/IncorrectTypeHintException.java @@ -0,0 +1,32 @@ +/* + * 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.MethodNode; +import org.codehaus.groovy.syntax.SyntaxException; + +public class IncorrectTypeHintException extends SyntaxException { + public IncorrectTypeHintException(final MethodNode mn, final Throwable e, int line, int column) { + super("Incorrect type hint in @ClosureParams in class "+mn.getDeclaringClass().getName()+" method "+mn.getTypeDescriptor()+" : "+e.getMessage(), e, line, column); + } + + public IncorrectTypeHintException(final MethodNode mn, final String msg, final int line, final int column) { + super("Incorrect type hint in @ClosureParams in class "+mn.getDeclaringClass().getName()+" method "+mn.getTypeDescriptor()+" : "+msg, line, column); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/MapEntryOrKeyValue.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/MapEntryOrKeyValue.java b/src/main/groovy/groovy/transform/stc/MapEntryOrKeyValue.java new file mode 100644 index 0000000..9316ef8 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/MapEntryOrKeyValue.java @@ -0,0 +1,119 @@ +/* + * 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.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * <p>A special hint which handles a common use case in the Groovy methods that work on maps. In case of an + * iteration on a list of map entries, you often want the user to be able to work either on a {@link java.util.Map.Entry} map entry + * or on a key,value pair.</p> + * <p>The result is a closure which can have the following forms:</p> + * <ul> + * <li><code>{ key, value -> ...}</code> where key is the key of a map entry, and value the corresponding value</li> + * <li><code>{ entry -> ... }</code> where entry is a {@link java.util.Map.Entry} map entry</li> + * <li><code>{ ...}</code> where <i>it</i> is an implicit {@link java.util.Map.Entry} map entry</li> + * </ul> + * <p>This hint handles all those cases by picking the generics from the first argument of the method (by default).</p> + * <p>The options array is used to modify the behavior of this hint. Each string in the option array consists of + * a key=value pair.</p> + * <ul> + * <li><i>argNum=index</i> of the parameter representing the map (by default, 0)</li> + * <li><i>index=true or false</i>, by default false. If true, then an additional "int" parameter is added, + * for "withIndex" variants</li> + * </ul> + * <code>void doSomething(String str, Map<K,>V map, @ClosureParams(value=MapEntryOrKeyValue.class,options="argNum=1") Closure c) { ... }</code> + */ +public class MapEntryOrKeyValue extends ClosureSignatureHint { + private static final ClassNode MAPENTRY_TYPE = ClassHelper.make(Map.Entry.class); + + public List<ClassNode[]> getClosureSignatures(final MethodNode node, final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final String[] options, final ASTNode usage) { + Options opt; + try { + opt = Options.parse(node, usage, options); + } catch (IncorrectTypeHintException e) { + sourceUnit.addError(e); + return Collections.emptyList(); + } + GenericsType[] genericsTypes = node.getParameters()[opt.parameterIndex].getOriginType().getGenericsTypes(); + if (genericsTypes==null) { + // would happen if you have a raw Map type for example + genericsTypes = new GenericsType[] { + new GenericsType(ClassHelper.OBJECT_TYPE), + new GenericsType(ClassHelper.OBJECT_TYPE) + }; + } + ClassNode[] firstSig; + ClassNode[] secondSig; + ClassNode mapEntry = MAPENTRY_TYPE.getPlainNodeReference(); + mapEntry.setGenericsTypes(genericsTypes); + if (opt.generateIndex) { + firstSig = new ClassNode[] {genericsTypes[0].getType(), genericsTypes[1].getType(), ClassHelper.int_TYPE}; + secondSig = new ClassNode[] {mapEntry, ClassHelper.int_TYPE}; + + } else { + firstSig = new ClassNode[] {genericsTypes[0].getType(), genericsTypes[1].getType()}; + secondSig = new ClassNode[] {mapEntry}; + } + return Arrays.asList(firstSig, secondSig); + } + + private static final class Options { + final int parameterIndex; + final boolean generateIndex; + + private Options(final int parameterIndex, final boolean generateIndex) { + this.parameterIndex = parameterIndex; + this.generateIndex = generateIndex; + } + + static Options parse(MethodNode mn, ASTNode source, String[] options) throws IncorrectTypeHintException { + int pIndex = 0; + boolean generateIndex = false; + for (String option : options) { + String[] keyValue = option.split("="); + if (keyValue.length==2) { + String key = keyValue[0]; + String value = keyValue[1]; + if ("argNum".equals(key)) { + pIndex = Integer.parseInt(value); + } else if ("index".equals(key)) { + generateIndex = Boolean.valueOf(value); + } else { + throw new IncorrectTypeHintException(mn, "Unrecognized option: "+key, source.getLineNumber(), source.getColumnNumber()); + } + } else { + throw new IncorrectTypeHintException(mn, "Incorrect option format. Should be argNum=<num> or index=<boolean> ", source.getLineNumber(), source.getColumnNumber()); + } + } + return new Options(pIndex, generateIndex); + } + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/PickAnyArgumentHint.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/PickAnyArgumentHint.java b/src/main/groovy/groovy/transform/stc/PickAnyArgumentHint.java new file mode 100644 index 0000000..6a5463f --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/PickAnyArgumentHint.java @@ -0,0 +1,75 @@ +/* + * 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>Base class for hints which use the type of a parameter of the annotated method as the signature. + * This can optionally use a generic type of the selected parameter as the hint. For example, imagine the following + * method:</p> + * <code>void foo(A firstArg, B secondArg, Closure c) {...}</code> + * <p>If the <i>c</i> closure should be <code>{ B it -> ...}</code>, then we can see that the parameter type + * should be picked from the second parameter of the foo method, which is what {@link groovy.transform.stc.PickAnyArgumentHint} + * lets you do.</p> + * <p>Alternatively, the method may look like this:</p> + * <code>void <T> foo(A<T> firstArg, B secondArg, Closure c) {...}</code> + * <p>in which case if you want to express the fact that <i>c</i> should accept a <T> then you can use the + * {@link #genericTypeIndex} value.</p> + * <p></p> + * <p>This class is extended by several hint providers that make it easier to use as annotation values.</p> + * + * @author Cédric Champeau + * @since 2.3.0 + */ +public class PickAnyArgumentHint extends SingleSignatureClosureHint { + private final int parameterIndex; + private final int genericTypeIndex; + + /** + * Creates the an argument picker which extracts the type of the first parameter. + */ + public PickAnyArgumentHint() { + this(0,-1); + } + + /** + * Creates a picker which will extract the parameterIndex-th parameter type, or its + * genericTypeIndex-th generic type genericTypeIndex is >=0. + * @param parameterIndex the index of the parameter from which to extract the type + * @param genericTypeIndex if >=0, then returns the corresponding generic type instead of the parameter type. + */ + public PickAnyArgumentHint(final int parameterIndex, final int genericTypeIndex) { + this.parameterIndex = parameterIndex; + this.genericTypeIndex = genericTypeIndex; + } + + @Override + public ClassNode[] getParameterTypes(final MethodNode node, final String[] options, final SourceUnit sourceUnit, final CompilationUnit unit, final ASTNode usage) { + ClassNode type = node.getParameters()[parameterIndex].getOriginType(); + if (genericTypeIndex>=0) { + type = pickGenericType(type, genericTypeIndex); + } + return new ClassNode[]{type}; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/PickFirstResolver.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/PickFirstResolver.java b/src/main/groovy/groovy/transform/stc/PickFirstResolver.java new file mode 100644 index 0000000..0bebb3e --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/PickFirstResolver.java @@ -0,0 +1,45 @@ +/* + * 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.Collections; +import java.util.List; + +/** + * Returns the first of several candidates found. + * This is useful if several types should be supported but only the first + * should be the default/inferred type. Other options in the list are + * obtained through explicitly typing the parameter(s). + * + * @since 2.5.0 + */ +public class PickFirstResolver extends ClosureSignatureConflictResolver { + @Override + public List<ClassNode[]> resolve(List<ClassNode[]> candidates, ClassNode receiver, Expression arguments, ClosureExpression closure, + MethodNode methodNode, SourceUnit sourceUnit, CompilationUnit compilationUnit, String[] options) { + return Collections.singletonList(candidates.get(0)); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/SecondParam.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/SecondParam.java b/src/main/groovy/groovy/transform/stc/SecondParam.java new file mode 100644 index 0000000..c077750 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/SecondParam.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 second parameter type. For example:</p> + * <code>public <T,U> def doWith(T first, U second, @ClosureParams(SecondParam.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 SecondParam extends PickAnyArgumentHint { + public SecondParam() { + super(1,-1); + } + + /** + * <p>A hint used to instruct the type checker to pick the first generic type of the second parameter type. For example:</p> + * <code>void <T> doWithElements(String base, List<T> src, @ClosureParams(SecondParam.FirstGenericType.class) Closure c) { ... } }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class FirstGenericType extends PickAnyArgumentHint { + public FirstGenericType() { + super(1,0); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the second generic type of the second parameter type. For example:</p> + * <code>void <T,U> doWithElements(String base, Tuple<T,U> src, @ClosureParams(SecondParam.SecondGenericType.class) Closure c) { ... }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class SecondGenericType extends PickAnyArgumentHint { + public SecondGenericType() { + super(1,1); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the second generic type of the second parameter type. For example:</p> + * <code>void <T,U,V> doWithElements(String base, Triple<T,U,V> src, @ClosureParams(SecondParam.ThirdGenericType.class) Closure c) { ... }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class ThirdGenericType extends PickAnyArgumentHint { + public ThirdGenericType() { + super(1,2); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the type of the component of the second parameter type, which is therefore + * expected to be an array, like in this example:</p> + * <code>void <T> doWithArray(String first, T[] array, @ClosureParams(FirstParam.Component.class) Closure c) { ... }</code> + */ + public static class Component extends SecondParam { + @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/d638ca43/src/main/groovy/groovy/transform/stc/SimpleType.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/SimpleType.java b/src/main/groovy/groovy/transform/stc/SimpleType.java new file mode 100644 index 0000000..8866e3b --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/SimpleType.java @@ -0,0 +1,36 @@ +/* + * 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; + +public class SimpleType extends SingleSignatureClosureHint { + @Override + public ClassNode[] getParameterTypes(final MethodNode node, final String[] options, final SourceUnit sourceUnit, final CompilationUnit unit, final ASTNode usage) { + ClassNode[] result = new ClassNode[options.length]; + for (int i = 0; i < result.length; i++) { + result[i] = findClassNode(sourceUnit, unit, options[i]); + } + return result; + } + } http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/SingleSignatureClosureHint.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/SingleSignatureClosureHint.java b/src/main/groovy/groovy/transform/stc/SingleSignatureClosureHint.java new file mode 100644 index 0000000..3db66ff --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/SingleSignatureClosureHint.java @@ -0,0 +1,44 @@ +/* + * 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; + +import java.util.Collections; +import java.util.List; + +/** + * A simplified version of a {@link groovy.transform.stc.ClosureSignatureHint} which is suitable + * for monomorphic closures, that is to say closures which only respond to a single signature. + * + * @author Cédric Champeau + * @since 2.3.0 + */ +public abstract class SingleSignatureClosureHint extends ClosureSignatureHint { + + public abstract ClassNode[] getParameterTypes(final MethodNode node, final String[] options, final SourceUnit sourceUnit, final CompilationUnit unit, final ASTNode usage); + + public List<ClassNode[]> getClosureSignatures(final MethodNode node, final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final String[] options, final ASTNode usage) { + return Collections.singletonList(getParameterTypes(node, options, sourceUnit, compilationUnit, usage)); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/stc/ThirdParam.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/stc/ThirdParam.java b/src/main/groovy/groovy/transform/stc/ThirdParam.java new file mode 100644 index 0000000..c493e36 --- /dev/null +++ b/src/main/groovy/groovy/transform/stc/ThirdParam.java @@ -0,0 +1,94 @@ +/* + * 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 third parameter type. For example:</p> + * <code>public <T,U,V> def doWith(T first, U second, V third, @ClosureParams(ThirdParam.class) Closure c) { ... }</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 ThirdParam extends PickAnyArgumentHint { + public ThirdParam() { + super(2,-1); + } + + /** + * <p>A hint used to instruct the type checker to pick the first generic type of the third parameter type. For example:</p> + * <code>void <T> doWithElements(String first, Integer second, List<T> third, @ClosureParams(SecondParam.FirstGenericType.class) Closure c) { ... } }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class FirstGenericType extends PickAnyArgumentHint { + public FirstGenericType() { + super(2,0); + } + } + + + /** + * <p>A hint used to instruct the type checker to pick the second generic type of the third parameter type. For example:</p> + * <code>void <T,U> doWithElements(String first, Integer second, Tuple<T,U> third, @ClosureParams(SecondParam.SecondGenericType.class) Closure c) { ... }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class SecondGenericType extends PickAnyArgumentHint { + public SecondGenericType() { + super(2,1); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the second generic type of the third parameter type. For example:</p> + * <code>void <T,U,V> doWithElements(String first, Integer second, Triple<T,U,V> src, @ClosureParams(SecondParam.ThirdGenericType.class) Closure c) { ... }</code> + * + * @author Cédric Champeau + * @since 2.3.0 + */ + public static class ThirdGenericType extends PickAnyArgumentHint { + public ThirdGenericType() { + super(2,2); + } + } + + /** + * <p>A hint used to instruct the type checker to pick the type of the component of the third parameter type, which is therefore + * expected to be an array, like in this example:</p> + * <code>void <T> doWithArray(String first, int second, T[] third, @ClosureParams(FirstParam.Component.class) Closure c) { ... }</code> + */ + public static class Component extends ThirdParam { + @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; + } + } +}
