diff options
-rw-r--r-- | src/core/lombok/Builder.java | 32 | ||||
-rw-r--r-- | src/core/lombok/eclipse/handlers/HandleBuilder.java | 108 | ||||
-rw-r--r-- | src/core/lombok/eclipse/handlers/HandleConstructor.java | 72 | ||||
-rw-r--r-- | src/core/lombok/javac/handlers/HandleBuilder.java | 115 | ||||
-rw-r--r-- | src/core/lombok/javac/handlers/HandleConstructor.java | 79 |
5 files changed, 329 insertions, 77 deletions
diff --git a/src/core/lombok/Builder.java b/src/core/lombok/Builder.java index 7a965486..9cf82191 100644 --- a/src/core/lombok/Builder.java +++ b/src/core/lombok/Builder.java @@ -123,6 +123,38 @@ public @interface Builder { String builderClassName() default ""; /** + * If true, the generated builder class will extend the {@code @Builder} of the + * superclass. In this way, the builder will also contain methods for fields + * from the superclass.<br> + * Note that both this builder and the superclass' builder must be a type + * {@code @Builder}; this feature does neither work for constructor nor + * method {@code @Builder}s. The parent {@code @Builder} must be + * {@link #extendable()}. <br> + * Implies {@link #extendable()}. + */ + boolean inherit() default false; + + /** + * If true, the generated builder pattern will be extendable by + * {@code @Builder}s of subclasses, using the {@link #inherit()} feature. + * This is achieved by generating a special constructor that takes a builder + * instance as parameter (instead of an {@link AllArgsConstructor}). <br> + * Note that this builder must be a type {@code @Builder}; this feature does + * neither work for constructor nor method {@code @Builder}s.<br> + * If {@link #inherit()} {@code == true}, {@link #extendable()} will + * automatically be {@code true}. + */ + boolean extendable() default false; + + /** + * Name of the builder class in the superclass. Only relevant if + * {@code inherit = true} (see {@link #inherit()}). + * + * Default {@code (SuperclassTypeName)Builder}. + */ + String superclassBuilderClassName() default ""; + + /** * If true, generate an instance method to obtain a builder that is initialized with the values of this instance. * Legal only if {@code @Builder} is used on a constructor, on the type itself, or on a static method that returns * an instance of the declaring type. diff --git a/src/core/lombok/eclipse/handlers/HandleBuilder.java b/src/core/lombok/eclipse/handlers/HandleBuilder.java index 8c200a94..58e08b0d 100644 --- a/src/core/lombok/eclipse/handlers/HandleBuilder.java +++ b/src/core/lombok/eclipse/handlers/HandleBuilder.java @@ -139,6 +139,11 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { String builderMethodName = builderInstance.builderMethodName(); String buildMethodName = builderInstance.buildMethodName(); String builderClassName = builderInstance.builderClassName(); + + boolean inherit = builderInstance.inherit(); + boolean extendable = inherit || builderInstance.extendable(); // inherit implies extendable + String superclassBuilderClassName = builderInstance.superclassBuilderClassName(); + String toBuilderMethodName = "toBuilder"; boolean toBuilder = builderInstance.toBuilder(); List<char[]> typeArgsForToBuilder = null; @@ -146,6 +151,9 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { if (builderMethodName == null) builderMethodName = "builder"; if (buildMethodName == null) builderMethodName = "build"; if (builderClassName == null) builderClassName = ""; + if (superclassBuilderClassName == null) { + superclassBuilderClassName = ""; + } if (!checkName("builderMethodName", builderMethodName, annotationNode)) return; if (!checkName("buildMethodName", buildMethodName, annotationNode)) return; @@ -189,15 +197,30 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { allFields.add(fieldNode); } - new HandleConstructor().generateConstructor(tdParent, AccessLevel.PACKAGE, allFields, false, null, SkipIfConstructorExists.I_AM_BUILDER, - Collections.<Annotation>emptyList(), annotationNode); + if (builderClassName.isEmpty()) { + builderClassName = new String(td.name) + "Builder"; + } + if (superclassBuilderClassName.isEmpty() && td.superclass != null) { + superclassBuilderClassName = new String(td.superclass.getLastToken()) + "Builder"; + } + + boolean callBuilderBasedSuperConstructor = inherit && td.superclass != null; + new HandleConstructor().generateConstructor(tdParent, extendable ? AccessLevel.PROTECTED : AccessLevel.PACKAGE, allFields, false, null, SkipIfConstructorExists.I_AM_BUILDER, + Collections.<Annotation>emptyList(), annotationNode, extendable ? builderClassName : null, callBuilderBasedSuperConstructor); returnType = namePlusTypeParamsToTypeReference(td.name, td.typeParameters, p); typeParams = td.typeParameters; thrownExceptions = null; nameOfStaticBuilderMethod = null; - if (builderClassName.isEmpty()) builderClassName = new String(td.name) + "Builder"; } else if (parent.get() instanceof ConstructorDeclaration) { + if (inherit) { + annotationNode.addError("@Builder(inherit=true) is only supported for type builders."); + return; + } + if (extendable) { + annotationNode.addError("@Builder(extendable=true) is only supported for type builders."); + return; + } ConstructorDeclaration cd = (ConstructorDeclaration) parent.get(); if (cd.typeParameters != null && cd.typeParameters.length > 0) { annotationNode.addError("@Builder is not supported on constructors with constructor type parameters."); @@ -212,6 +235,14 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { nameOfStaticBuilderMethod = null; if (builderClassName.isEmpty()) builderClassName = new String(cd.selector) + "Builder"; } else if (parent.get() instanceof MethodDeclaration) { + if (inherit) { + annotationNode.addError("@Builder(inherit=true) is only supported for type builders."); + return; + } + if (extendable) { + annotationNode.addError("@Builder(extendable=true) is only supported for type builders."); + return; + } MethodDeclaration md = (MethodDeclaration) parent.get(); tdParent = parent.up(); isStatic = md.isStatic(); @@ -341,7 +372,7 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { EclipseNode builderType = findInnerClass(tdParent, builderClassName); if (builderType == null) { - builderType = makeBuilderClass(isStatic, tdParent, builderClassName, typeParams, ast); + builderType = makeBuilderClass(isStatic, tdParent, builderClassName, typeParams, ast, inherit ? superclassBuilderClassName : null); } else { TypeDeclaration builderTypeDeclaration = (TypeDeclaration) builderType.get(); if (isStatic && (builderTypeDeclaration.modifiers & ClassFileConstants.AccStatic) == 0) { @@ -396,7 +427,7 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { if (constructorExists(builderType) == MemberExistsResult.NOT_EXISTS) { ConstructorDeclaration cd = HandleConstructor.createConstructor( AccessLevel.PACKAGE, builderType, Collections.<EclipseNode>emptyList(), false, - annotationNode, Collections.<Annotation>emptyList()); + annotationNode, Collections.<Annotation>emptyList(), null, false); if (cd != null) injectMethod(builderType, cd); } @@ -405,7 +436,8 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { } if (methodExists(buildMethodName, builderType, -1) == MemberExistsResult.NOT_EXISTS) { - MethodDeclaration md = generateBuildMethod(isStatic, buildMethodName, nameOfStaticBuilderMethod, returnType, builderFields, builderType, thrownExceptions, addCleaning, ast); + boolean useBuilderBasedConstructor = parent.get() instanceof TypeDeclaration && extendable; + MethodDeclaration md = generateBuildMethod(isStatic, buildMethodName, nameOfStaticBuilderMethod, returnType, builderFields, builderType, thrownExceptions, addCleaning, ast, useBuilderBasedConstructor); if (md != null) injectMethod(builderType, md); } @@ -514,35 +546,44 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { return decl; } - public MethodDeclaration generateBuildMethod(boolean isStatic, String name, char[] staticName, TypeReference returnType, List<BuilderFieldData> builderFields, EclipseNode type, TypeReference[] thrownExceptions, boolean addCleaning, ASTNode source) { + /** + * @param useBuilderBasedConstructor + * if true, the {@code build()} method will use a constructor + * that takes the builder instance as parameter (instead of a + * constructor with all relevant fields as parameters) + */ + public MethodDeclaration generateBuildMethod(boolean isStatic, String name, char[] staticName, TypeReference returnType, List<BuilderFieldData> builderFields, EclipseNode type, TypeReference[] thrownExceptions, boolean addCleaning, ASTNode source, boolean useBuilderBasedConstructor) { MethodDeclaration out = new MethodDeclaration(((CompilationUnitDeclaration) type.top().get()).compilationResult); out.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG; List<Statement> statements = new ArrayList<Statement>(); + List<Expression> args = new ArrayList<Expression>(); - if (addCleaning) { - FieldReference thisUnclean = new FieldReference(CLEAN_FIELD_NAME, 0); - thisUnclean.receiver = new ThisReference(0, 0); - Expression notClean = new UnaryExpression(thisUnclean, OperatorIds.NOT); - MessageSend invokeClean = new MessageSend(); - invokeClean.selector = CLEAN_METHOD_NAME; - statements.add(new IfStatement(notClean, invokeClean, 0, 0)); - } + // Extendable builders assign their values in the constructor, not in this build() method. + if (!useBuilderBasedConstructor) { + if (addCleaning) { + FieldReference thisUnclean = new FieldReference(CLEAN_FIELD_NAME, 0); + thisUnclean.receiver = new ThisReference(0, 0); + Expression notClean = new UnaryExpression(thisUnclean, OperatorIds.NOT); + MessageSend invokeClean = new MessageSend(); + invokeClean.selector = CLEAN_METHOD_NAME; + statements.add(new IfStatement(notClean, invokeClean, 0, 0)); + } - for (BuilderFieldData bfd : builderFields) { - if (bfd.singularData != null && bfd.singularData.getSingularizer() != null) { - bfd.singularData.getSingularizer().appendBuildCode(bfd.singularData, type, statements, bfd.name); + for (BuilderFieldData bfd : builderFields) { + if (bfd.singularData != null && bfd.singularData.getSingularizer() != null) { + bfd.singularData.getSingularizer().appendBuildCode(bfd.singularData, type, statements, bfd.name); + } } - } - List<Expression> args = new ArrayList<Expression>(); - for (BuilderFieldData bfd : builderFields) { - args.add(new SingleNameReference(bfd.name, 0L)); - } + for (BuilderFieldData bfd : builderFields) { + args.add(new SingleNameReference(bfd.name, 0L)); + } - if (addCleaning) { - FieldReference thisUnclean = new FieldReference(CLEAN_FIELD_NAME, 0); - thisUnclean.receiver = new ThisReference(0, 0); - statements.add(new Assignment(thisUnclean, new TrueLiteral(0, 0), 0)); + if (addCleaning) { + FieldReference thisUnclean = new FieldReference(CLEAN_FIELD_NAME, 0); + thisUnclean.receiver = new ThisReference(0, 0); + statements.add(new Assignment(thisUnclean, new TrueLiteral(0, 0), 0)); + } } out.modifiers = ClassFileConstants.AccPublic; @@ -554,7 +595,13 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { if (staticName == null) { AllocationExpression allocationStatement = new AllocationExpression(); allocationStatement.type = copyType(out.returnType); - allocationStatement.arguments = args.isEmpty() ? null : args.toArray(new Expression[args.size()]); + if (useBuilderBasedConstructor) { + // Use a constructor that only has this builder as parameter. + allocationStatement.arguments = new Expression[] {new ThisReference(0, 0)}; + } else { + // Use a constructor with all the fields. + allocationStatement.arguments = args.isEmpty() ? null : args.toArray(new Expression[args.size()]); + } statements.add(new ReturnStatement(allocationStatement, 0, 0)); } else { MessageSend invoke = new MessageSend(); @@ -672,7 +719,7 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { return null; } - public EclipseNode makeBuilderClass(boolean isStatic, EclipseNode tdParent, String builderClassName, TypeParameter[] typeParams, ASTNode source) { + public EclipseNode makeBuilderClass(boolean isStatic, EclipseNode tdParent, String builderClassName, TypeParameter[] typeParams, ASTNode source, String parentBuilderClassName) { TypeDeclaration parent = (TypeDeclaration) tdParent.get(); TypeDeclaration builder = new TypeDeclaration(parent.compilationResult); builder.bits |= Eclipse.ECLIPSE_DO_NOT_TOUCH_FLAG; @@ -680,6 +727,9 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { if (isStatic) builder.modifiers |= ClassFileConstants.AccStatic; builder.typeParameters = copyTypeParams(typeParams, source); builder.name = builderClassName.toCharArray(); + if (parentBuilderClassName != null) { + builder.superclass = new SingleTypeReference(parentBuilderClassName.toCharArray(), 0); + } builder.traverse(new SetGeneratedByVisitor(source), (ClassScope) null); return injectType(tdParent, builder); } diff --git a/src/core/lombok/eclipse/handlers/HandleConstructor.java b/src/core/lombok/eclipse/handlers/HandleConstructor.java index 49b09231..a8b823b2 100644 --- a/src/core/lombok/eclipse/handlers/HandleConstructor.java +++ b/src/core/lombok/eclipse/handlers/HandleConstructor.java @@ -64,10 +64,12 @@ import org.eclipse.jdt.internal.compiler.ast.IntLiteral; import org.eclipse.jdt.internal.compiler.ast.LongLiteral; import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration; import org.eclipse.jdt.internal.compiler.ast.NullLiteral; +import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference; import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference; import org.eclipse.jdt.internal.compiler.ast.ReturnStatement; import org.eclipse.jdt.internal.compiler.ast.SingleMemberAnnotation; import org.eclipse.jdt.internal.compiler.ast.SingleNameReference; +import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference; import org.eclipse.jdt.internal.compiler.ast.Statement; import org.eclipse.jdt.internal.compiler.ast.StringLiteral; import org.eclipse.jdt.internal.compiler.ast.ThisReference; @@ -95,7 +97,7 @@ public class HandleConstructor { List<EclipseNode> fields = force ? findFinalFields(typeNode) : Collections.<EclipseNode>emptyList(); List<Annotation> onConstructor = unboxAndRemoveAnnotationParameter(ast, "onConstructor", "@NoArgsConstructor(onConstructor", annotationNode); - new HandleConstructor().generateConstructor(typeNode, level, fields, force, staticName, SkipIfConstructorExists.NO, onConstructor, annotationNode); + new HandleConstructor().generateConstructor(typeNode, level, fields, force, staticName, SkipIfConstructorExists.NO, onConstructor, annotationNode, null, false); } } @@ -118,7 +120,7 @@ public class HandleConstructor { new HandleConstructor().generateConstructor( typeNode, level, findRequiredFields(typeNode), false, staticName, SkipIfConstructorExists.NO, - onConstructor, annotationNode); + onConstructor, annotationNode, null, false); } } @@ -177,7 +179,7 @@ public class HandleConstructor { new HandleConstructor().generateConstructor( typeNode, level, findAllFields(typeNode), false, staticName, SkipIfConstructorExists.NO, - onConstructor, annotationNode); + onConstructor, annotationNode, null, false); } } @@ -199,23 +201,34 @@ public class HandleConstructor { EclipseNode typeNode, AccessLevel level, String staticName, SkipIfConstructorExists skipIfConstructorExists, List<Annotation> onConstructor, EclipseNode sourceNode) { - generateConstructor(typeNode, level, findRequiredFields(typeNode), false, staticName, skipIfConstructorExists, onConstructor, sourceNode); + generateConstructor(typeNode, level, findRequiredFields(typeNode), false, staticName, skipIfConstructorExists, onConstructor, sourceNode, null, false); } public void generateAllArgsConstructor( EclipseNode typeNode, AccessLevel level, String staticName, SkipIfConstructorExists skipIfConstructorExists, List<Annotation> onConstructor, EclipseNode sourceNode) { - generateConstructor(typeNode, level, findAllFields(typeNode), false, staticName, skipIfConstructorExists, onConstructor, sourceNode); + generateConstructor(typeNode, level, findAllFields(typeNode), false, staticName, skipIfConstructorExists, onConstructor, sourceNode, null, false); } public enum SkipIfConstructorExists { YES, NO, I_AM_BUILDER; } + /** + * @param builderClassnameAsParameter + * if {@code != null}, the only parameter of the constructor will + * be a builder with this classname; the constructor will then + * use the values within this builder to assign the fields of new + * instances. + * @param callBuilderBasedSuperConstructor + * if {@code true}, the constructor will explicitly call a super + * constructor with the builder as argument. Requires + * {@code builderClassAsParameter != null}. + */ public void generateConstructor( EclipseNode typeNode, AccessLevel level, List<EclipseNode> fields, boolean allToDefault, String staticName, SkipIfConstructorExists skipIfConstructorExists, - List<Annotation> onConstructor, EclipseNode sourceNode) { + List<Annotation> onConstructor, EclipseNode sourceNode, String builderClassnameAsParameter, boolean callBuilderBasedSuperConstructor) { ASTNode source = sourceNode.get(); boolean staticConstrRequired = staticName != null && !staticName.equals(""); @@ -250,7 +263,7 @@ public class HandleConstructor { ConstructorDeclaration constr = createConstructor( staticConstrRequired ? AccessLevel.PRIVATE : level, typeNode, fields, allToDefault, - sourceNode, onConstructor); + sourceNode, onConstructor, builderClassnameAsParameter, callBuilderBasedSuperConstructor); injectMethod(typeNode, constr); if (staticConstrRequired) { MethodDeclaration staticConstr = createStaticConstructor(level, staticName, typeNode, allToDefault ? Collections.<EclipseNode>emptyList() : fields, source); @@ -290,9 +303,28 @@ public class HandleConstructor { return new Annotation[] { ann }; } + /** + * @param builderClassnameAsParameter + * if {@code != null}, the only parameter of the constructor will + * be a builder with this classname; the constructor will then + * use the values within this builder to assign the fields of new + * instances. + * @param callBuilderBasedSuperConstructor + * if {@code true}, the constructor will explicitly call a super + * constructor with the builder as argument. Requires + * {@code builderClassnameAsParameter != null}. + */ public static ConstructorDeclaration createConstructor( AccessLevel level, EclipseNode type, Collection<EclipseNode> fields, boolean allToDefault, - EclipseNode sourceNode, List<Annotation> onConstructor) { + EclipseNode sourceNode, List<Annotation> onConstructor, + String builderClassnameAsParameter, boolean callBuilderBasedSuperConstructor) { + + if (builderClassnameAsParameter != null && builderClassnameAsParameter.isEmpty()) { + builderClassnameAsParameter = null; + } + if (callBuilderBasedSuperConstructor && builderClassnameAsParameter == null) { + type.addError("Calling a builder-based superclass constructor ('callBuilderBasedSuperConstructor') requires a non-empty 'builderClassnameAsParameter' value."); + } ASTNode source = sourceNode.get(); TypeDeclaration typeDeclaration = ((TypeDeclaration) type.get()); @@ -313,7 +345,12 @@ public class HandleConstructor { constructor.modifiers = toEclipseModifier(level); constructor.selector = typeDeclaration.name; - constructor.constructorCall = new ExplicitConstructorCall(ExplicitConstructorCall.ImplicitSuper); + if (callBuilderBasedSuperConstructor) { + constructor.constructorCall = new ExplicitConstructorCall(ExplicitConstructorCall.Super); + constructor.constructorCall.arguments = new Expression[] {new SingleNameReference("b".toCharArray(), p)}; + } else { + constructor.constructorCall = new ExplicitConstructorCall(ExplicitConstructorCall.ImplicitSuper); + } constructor.constructorCall.sourceStart = source.sourceStart; constructor.constructorCall.sourceEnd = source.sourceEnd; constructor.thrownExceptions = null; @@ -336,7 +373,16 @@ public class HandleConstructor { int e = (int) p; thisX.receiver = new ThisReference(s, e); - Expression assignmentExpr = allToDefault ? getDefaultExpr(field.type, s, e) : new SingleNameReference(fieldName, p); + Expression assignmentExpr; + if (allToDefault) { + assignmentExpr = getDefaultExpr(field.type, s, e); + } else if (builderClassnameAsParameter != null) { + char[][] variableInBuilder = new char[][] {"b".toCharArray(), fieldName}; + long[] positions = new long[] {p, p}; + assignmentExpr = new QualifiedNameReference(variableInBuilder, positions, s, e); + } else { + assignmentExpr = new SingleNameReference(fieldName, p); + } Assignment assignment = new Assignment(thisX, assignmentExpr, (int) p); assignment.sourceStart = (int) (p >> 32); assignment.sourceEnd = assignment.statementEnd = (int) (p >> 32); @@ -357,7 +403,11 @@ public class HandleConstructor { nullChecks.addAll(assigns); constructor.statements = nullChecks.isEmpty() ? null : nullChecks.toArray(new Statement[nullChecks.size()]); - constructor.arguments = params.isEmpty() ? null : params.toArray(new Argument[params.size()]); + if (builderClassnameAsParameter != null) { + constructor.arguments = new Argument[] {new Argument("b".toCharArray(), p, new SingleTypeReference(builderClassnameAsParameter.toCharArray(), p), Modifier.FINAL)}; + } else { + constructor.arguments = params.isEmpty() ? null : params.toArray(new Argument[params.size()]); + } /* Generate annotations that must be put on the generated method, and attach them. */ { Annotation[] constructorProperties = null; diff --git a/src/core/lombok/javac/handlers/HandleBuilder.java b/src/core/lombok/javac/handlers/HandleBuilder.java index 4c670433..43520656 100644 --- a/src/core/lombok/javac/handlers/HandleBuilder.java +++ b/src/core/lombok/javac/handlers/HandleBuilder.java @@ -22,7 +22,10 @@ package lombok.javac.handlers; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Consumer; import javax.lang.model.element.Modifier; @@ -50,6 +53,7 @@ import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.ListBuffer; import com.sun.tools.javac.util.Name; +import jdk.nashorn.internal.runtime.Context; import lombok.AccessLevel; import lombok.Builder; import lombok.Builder.ObtainVia; @@ -64,6 +68,7 @@ import lombok.javac.Javac; import lombok.javac.JavacAnnotationHandler; import lombok.javac.JavacNode; import lombok.javac.JavacTreeMaker; +import lombok.javac.JavacTreeMaker.TypeTag; import lombok.javac.handlers.HandleConstructor.SkipIfConstructorExists; import lombok.javac.handlers.JavacSingularsRecipes.JavacSingularizer; import lombok.javac.handlers.JavacSingularsRecipes.SingularData; @@ -103,12 +108,20 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { String buildMethodName = builderInstance.buildMethodName(); String builderClassName = builderInstance.builderClassName(); String toBuilderMethodName = "toBuilder"; + + boolean inherit = builderInstance.inherit(); + boolean extendable = inherit || builderInstance.extendable(); // inherit implies extendable + String superclassBuilderClassName = builderInstance.superclassBuilderClassName(); + boolean toBuilder = builderInstance.toBuilder(); java.util.List<Name> typeArgsForToBuilder = null; if (builderMethodName == null) builderMethodName = "builder"; if (buildMethodName == null) buildMethodName = "build"; if (builderClassName == null) builderClassName = ""; + if (superclassBuilderClassName == null) { + superclassBuilderClassName = ""; + } if (!checkName("builderMethodName", builderMethodName, annotationNode)) return; if (!checkName("buildMethodName", buildMethodName, annotationNode)) return; @@ -155,14 +168,31 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { allFields.append(fieldNode); } - new HandleConstructor().generateConstructor(tdParent, AccessLevel.PACKAGE, List.<JCAnnotation>nil(), allFields.toList(), false, null, SkipIfConstructorExists.I_AM_BUILDER, annotationNode); + if (builderClassName.isEmpty()) { + builderClassName = td.name.toString() + "Builder"; + } + + JCTree extendsClause = Javac.getExtendsClause(td); + if (superclassBuilderClassName.isEmpty() && extendsClause != null) { + superclassBuilderClassName = extendsClause + "Builder"; + } + + boolean callBuilderBasedSuperConstructor = inherit && extendsClause != null; + new HandleConstructor().generateConstructor(tdParent, AccessLevel.PROTECTED, List.<JCAnnotation>nil(), allFields.toList(), false, null, SkipIfConstructorExists.I_AM_BUILDER, annotationNode, extendable ? builderClassName : null, callBuilderBasedSuperConstructor); returnType = namePlusTypeParamsToTypeReference(tdParent.getTreeMaker(), td.name, td.typarams); typeParams = td.typarams; thrownExceptions = List.nil(); nameOfBuilderMethod = null; - if (builderClassName.isEmpty()) builderClassName = td.name.toString() + "Builder"; } else if (fillParametersFrom != null && fillParametersFrom.getName().toString().equals("<init>")) { + if (inherit) { + annotationNode.addError("@Builder(inherit=true) is only supported for type builders."); + return; + } + if (extendable) { + annotationNode.addError("@Builder(extendable=true) is only supported for type builders."); + return; + } JCMethodDecl jmd = (JCMethodDecl) fillParametersFrom.get(); if (!jmd.typarams.isEmpty()) { annotationNode.addError("@Builder is not supported on constructors with constructor type parameters."); @@ -177,6 +207,14 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { nameOfBuilderMethod = null; if (builderClassName.isEmpty()) builderClassName = td.name.toString() + "Builder"; } else if (fillParametersFrom != null) { + if (inherit) { + annotationNode.addError("@Builder(inherit=true) is only supported for type builders."); + return; + } + if (extendable) { + annotationNode.addError("@Builder(extendable=true) is only supported for type builders."); + return; + } tdParent = parent.up(); JCClassDecl td = (JCClassDecl) tdParent.get(); JCMethodDecl jmd = (JCMethodDecl) fillParametersFrom.get(); @@ -297,7 +335,7 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { JavacNode builderType = findInnerClass(tdParent, builderClassName); if (builderType == null) { - builderType = makeBuilderClass(isStatic, annotationNode, tdParent, builderClassName, typeParams, ast); + builderType = makeBuilderClass(isStatic, annotationNode, tdParent, builderClassName, typeParams, ast, inherit ? superclassBuilderClassName : null); } else { JCClassDecl builderTypeDeclaration = (JCClassDecl) builderType.get(); if (isStatic && !builderTypeDeclaration.getModifiers().getFlags().contains(Modifier.STATIC)) { @@ -348,7 +386,7 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { } if (constructorExists(builderType) == MemberExistsResult.NOT_EXISTS) { - JCMethodDecl cd = HandleConstructor.createConstructor(AccessLevel.PACKAGE, List.<JCAnnotation>nil(), builderType, List.<JavacNode>nil(), false, annotationNode); + JCMethodDecl cd = HandleConstructor.createConstructor(AccessLevel.PACKAGE, List.<JCAnnotation>nil(), builderType, List.<JavacNode>nil(), false, annotationNode, null, false); if (cd != null) injectMethod(builderType, cd); } @@ -357,7 +395,8 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { } if (methodExists(buildMethodName, builderType, -1) == MemberExistsResult.NOT_EXISTS) { - JCMethodDecl md = generateBuildMethod(isStatic, buildMethodName, nameOfBuilderMethod, returnType, builderFields, builderType, thrownExceptions, ast, addCleaning); + boolean useBuilderBasedConstructor = parent.get() instanceof JCClassDecl && extendable; + JCMethodDecl md = generateBuildMethod(isStatic, buildMethodName, nameOfBuilderMethod, returnType, builderFields, builderType, thrownExceptions, ast, addCleaning, useBuilderBasedConstructor); if (md != null) injectMethod(builderType, md); } @@ -494,36 +533,52 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { */ } - private JCMethodDecl generateBuildMethod(boolean isStatic, String buildName, Name builderName, JCExpression returnType, java.util.List<BuilderFieldData> builderFields, JavacNode type, List<JCExpression> thrownExceptions, JCTree source, boolean addCleaning) { + /** + * @param useBuilderBasedConstructor + * if true, the {@code build()} method will use a constructor + * that takes the builder instance as parameter (instead of a + * constructor with all relevant fields as parameters) + */ + private JCMethodDecl generateBuildMethod(boolean isStatic, String buildName, Name builderName, JCExpression returnType, java.util.List<BuilderFieldData> builderFields, JavacNode type, List<JCExpression> thrownExceptions, JCTree source, boolean addCleaning, boolean useBuilderBasedConstructor) { JavacTreeMaker maker = type.getTreeMaker(); JCExpression call; ListBuffer<JCStatement> statements = new ListBuffer<JCStatement>(); - - if (addCleaning) { - JCExpression notClean = maker.Unary(CTC_NOT, maker.Select(maker.Ident(type.toName("this")), type.toName("$lombokUnclean"))); - JCStatement invokeClean = maker.Exec(maker.Apply(List.<JCExpression>nil(), maker.Ident(type.toName("$lombokClean")), List.<JCExpression>nil())); - JCIf ifUnclean = maker.If(notClean, invokeClean, null); - statements.append(ifUnclean); - } - - for (BuilderFieldData bfd : builderFields) { - if (bfd.singularData != null && bfd.singularData.getSingularizer() != null) { - bfd.singularData.getSingularizer().appendBuildCode(bfd.singularData, type, source, statements, bfd.name); - } - } - ListBuffer<JCExpression> args = new ListBuffer<JCExpression>(); - for (BuilderFieldData bfd : builderFields) { - args.append(maker.Ident(bfd.name)); - } + + // Extendable builders assign their values in the constructor, not in this build() method. + if (!useBuilderBasedConstructor) { + if (addCleaning) { + JCExpression notClean = maker.Unary(CTC_NOT, maker.Select(maker.Ident(type.toName("this")), type.toName("$lombokUnclean"))); + JCStatement invokeClean = maker.Exec(maker.Apply(List.<JCExpression>nil(), maker.Ident(type.toName("$lombokClean")), List.<JCExpression>nil())); + JCIf ifUnclean = maker.If(notClean, invokeClean, null); + statements.append(ifUnclean); + } + + for (BuilderFieldData bfd : builderFields) { + if (bfd.singularData != null && bfd.singularData.getSingularizer() != null) { + bfd.singularData.getSingularizer().appendBuildCode(bfd.singularData, type, source, statements, bfd.name); + } + } - if (addCleaning) { - statements.append(maker.Exec(maker.Assign(maker.Select(maker.Ident(type.toName("this")), type.toName("$lombokUnclean")), maker.Literal(CTC_BOOLEAN, true)))); + for (BuilderFieldData bfd : builderFields) { + args.append(maker.Ident(bfd.name)); + } + + if (addCleaning) { + statements.append(maker.Exec(maker.Assign(maker.Select(maker.Ident(type.toName("this")), type.toName("$lombokUnclean")), maker.Literal(CTC_BOOLEAN, true)))); + } } if (builderName == null) { - call = maker.NewClass(null, List.<JCExpression>nil(), returnType, args.toList(), null); + if (useBuilderBasedConstructor) { + // Use a constructor that only has this builder as parameter. + List<JCExpression> builderArg = List.<JCExpression>of(maker.Ident(type.toName("this"))); + call = maker.NewClass(null, List.<JCExpression>nil(), returnType, builderArg, null); + } else { + // Use a constructor with all the fields. + call = maker.NewClass(null, List.<JCExpression>nil(), returnType, args.toList(), null); + } statements.append(maker.Return(call)); } else { @@ -627,12 +682,16 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { return null; } - public JavacNode makeBuilderClass(boolean isStatic, JavacNode source, JavacNode tdParent, String builderClassName, List<JCTypeParameter> typeParams, JCAnnotation ast) { + public JavacNode makeBuilderClass(boolean isStatic, JavacNode source, JavacNode tdParent, String builderClassName, List<JCTypeParameter> typeParams, JCAnnotation ast, String parentBuilderClassName) { JavacTreeMaker maker = tdParent.getTreeMaker(); int modifiers = Flags.PUBLIC; if (isStatic) modifiers |= Flags.STATIC; JCModifiers mods = maker.Modifiers(modifiers); - JCClassDecl builder = maker.ClassDef(mods, tdParent.toName(builderClassName), copyTypeParams(source, typeParams), null, List.<JCExpression>nil(), List.<JCTree>nil()); + JCExpression extending = null; + if (parentBuilderClassName != null) { + extending = maker.Ident(tdParent.toName(parentBuilderClassName)); + } + JCClassDecl builder = maker.ClassDef(mods, tdParent.toName(builderClassName), copyTypeParams(source, typeParams), extending, List.<JCExpression>nil(), List.<JCTree>nil()); return injectType(tdParent, builder); } diff --git a/src/core/lombok/javac/handlers/HandleConstructor.java b/src/core/lombok/javac/handlers/HandleConstructor.java index 4e90b639..06ac74bd 100644 --- a/src/core/lombok/javac/handlers/HandleConstructor.java +++ b/src/core/lombok/javac/handlers/HandleConstructor.java @@ -23,6 +23,9 @@ package lombok.javac.handlers; import static lombok.core.handlers.HandlerUtil.*; import static lombok.javac.handlers.JavacHandlerUtil.*; + +import java.util.Arrays; + import static lombok.javac.Javac.*; import lombok.AccessLevel; @@ -51,6 +54,7 @@ import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCExpression; import com.sun.tools.javac.tree.JCTree.JCFieldAccess; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; import com.sun.tools.javac.tree.JCTree.JCModifiers; import com.sun.tools.javac.tree.JCTree.JCPrimitiveTypeTree; import com.sun.tools.javac.tree.JCTree.JCReturn; @@ -78,7 +82,7 @@ public class HandleConstructor { String staticName = ann.staticName(); boolean force = ann.force(); List<JavacNode> fields = force ? findFinalFields(typeNode) : List.<JavacNode>nil(); - new HandleConstructor().generateConstructor(typeNode, level, onConstructor, fields, force, staticName, SkipIfConstructorExists.NO, annotationNode); + new HandleConstructor().generateConstructor(typeNode, level, onConstructor, fields, force, staticName, SkipIfConstructorExists.NO, annotationNode, null, false); } } @@ -100,7 +104,7 @@ public class HandleConstructor { annotationNode.addError("This deprecated feature is no longer supported. Remove it; you can create a lombok.config file with 'lombok.anyConstructor.suppressConstructorProperties = true'."); } - new HandleConstructor().generateConstructor(typeNode, level, onConstructor, findRequiredFields(typeNode), false, staticName, SkipIfConstructorExists.NO, annotationNode); + new HandleConstructor().generateConstructor(typeNode, level, onConstructor, findRequiredFields(typeNode), false, staticName, SkipIfConstructorExists.NO, annotationNode, null, false); } } @@ -146,7 +150,7 @@ public class HandleConstructor { if (annotation.isExplicit("suppressConstructorProperties")) { annotationNode.addError("This deprecated feature is no longer supported. Remove it; you can create a lombok.config file with 'lombok.anyConstructor.suppressConstructorProperties = true'."); } - new HandleConstructor().generateConstructor(typeNode, level, onConstructor, findAllFields(typeNode), false, staticName, SkipIfConstructorExists.NO, annotationNode); + new HandleConstructor().generateConstructor(typeNode, level, onConstructor, findAllFields(typeNode), false, staticName, SkipIfConstructorExists.NO, annotationNode, null, false); } } @@ -182,7 +186,7 @@ public class HandleConstructor { } public void generateRequiredArgsConstructor(JavacNode typeNode, AccessLevel level, String staticName, SkipIfConstructorExists skipIfConstructorExists, JavacNode source) { - generateConstructor(typeNode, level, List.<JCAnnotation>nil(), findRequiredFields(typeNode), false, staticName, skipIfConstructorExists, source); + generateConstructor(typeNode, level, List.<JCAnnotation>nil(), findRequiredFields(typeNode), false, staticName, skipIfConstructorExists, source, null, false); } public enum SkipIfConstructorExists { @@ -190,10 +194,21 @@ public class HandleConstructor { } public void generateAllArgsConstructor(JavacNode typeNode, AccessLevel level, String staticName, SkipIfConstructorExists skipIfConstructorExists, JavacNode source) { - generateConstructor(typeNode, level, List.<JCAnnotation>nil(), findAllFields(typeNode), false, staticName, skipIfConstructorExists, source); + generateConstructor(typeNode, level, List.<JCAnnotation>nil(), findAllFields(typeNode), false, staticName, skipIfConstructorExists, source, null, false); } - public void generateConstructor(JavacNode typeNode, AccessLevel level, List<JCAnnotation> onConstructor, List<JavacNode> fields, boolean allToDefault, String staticName, SkipIfConstructorExists skipIfConstructorExists, JavacNode source) { + /** + * @param builderClassnameAsParameter + * if {@code != null}, the only parameter of the constructor will + * be a builder with this classname; the constructor will then + * use the values within this builder to assign the fields of new + * instances. + * @param callBuilderBasedSuperConstructor + * if {@code true}, the constructor will explicitly call a super + * constructor with the builder as argument. Requires + * {@code builderClassAsParameter != null}. + */ + public void generateConstructor(JavacNode typeNode, AccessLevel level, List<JCAnnotation> onConstructor, List<JavacNode> fields, boolean allToDefault, String staticName, SkipIfConstructorExists skipIfConstructorExists, JavacNode source, String builderClassnameAsParameter, boolean callBuilderBasedSuperConstructor) { boolean staticConstrRequired = staticName != null && !staticName.equals(""); if (skipIfConstructorExists != SkipIfConstructorExists.NO && constructorExists(typeNode) != MemberExistsResult.NOT_EXISTS) return; @@ -222,7 +237,7 @@ public class HandleConstructor { } } - JCMethodDecl constr = createConstructor(staticConstrRequired ? AccessLevel.PRIVATE : level, onConstructor, typeNode, fields, allToDefault, source); + JCMethodDecl constr = createConstructor(staticConstrRequired ? AccessLevel.PRIVATE : level, onConstructor, typeNode, fields, allToDefault, source, builderClassnameAsParameter, callBuilderBasedSuperConstructor); ListBuffer<Type> argTypes = new ListBuffer<Type>(); for (JavacNode fieldNode : fields) { Type mirror = getMirrorForFieldType(fieldNode); @@ -256,7 +271,25 @@ public class HandleConstructor { mods.annotations = mods.annotations.append(annotation); } - public static JCMethodDecl createConstructor(AccessLevel level, List<JCAnnotation> onConstructor, JavacNode typeNode, List<JavacNode> fields, boolean allToDefault, JavacNode source) { + /** + * @param builderClassnameAsParameter + * if {@code != null}, the only parameter of the constructor will + * be a builder with this classname; the constructor will then + * use the values within this builder to assign the fields of new + * instances. + * @param callBuilderBasedSuperConstructor + * if {@code true}, the constructor will explicitly call a super + * constructor with the builder as argument. Requires + * {@code builderClassnameAsParameter != null}. + */ + public static JCMethodDecl createConstructor(AccessLevel level, List<JCAnnotation> onConstructor, JavacNode typeNode, List<JavacNode> fields, boolean allToDefault, JavacNode source, String builderClassnameAsParameter, boolean callBuilderBasedSuperConstructor) { + if (builderClassnameAsParameter != null && builderClassnameAsParameter.isEmpty()) { + builderClassnameAsParameter = null; + } + if (callBuilderBasedSuperConstructor && builderClassnameAsParameter == null) { + source.addError("Calling a builder-based superclass constructor ('callBuilderBasedSuperConstructor') requires a non-empty 'builderClassnameAsParameter' value."); + } + JavacTreeMaker maker = typeNode.getTreeMaker(); boolean isEnum = (((JCClassDecl) typeNode.get()).mods.flags & Flags.ENUM) != 0; @@ -274,6 +307,7 @@ public class HandleConstructor { ListBuffer<JCStatement> assigns = new ListBuffer<JCStatement>(); ListBuffer<JCVariableDecl> params = new ListBuffer<JCVariableDecl>(); + Name builderVariableName = typeNode.toName("b"); for (JavacNode fieldNode : fields) { JCVariableDecl field = (JCVariableDecl) fieldNode.get(); Name fieldName = removePrefixFromField(fieldNode); @@ -290,7 +324,17 @@ public class HandleConstructor { } } JCFieldAccess thisX = maker.Select(maker.Ident(fieldNode.toName("this")), rawName); - JCExpression assign = maker.Assign(thisX, allToDefault ? getDefaultExpr(maker, field.vartype) : maker.Ident(fieldName)); + + JCExpression rhs; + if (allToDefault) { + rhs = getDefaultExpr(maker, field.vartype); + } else if (builderClassnameAsParameter != null) { + rhs = maker.Select(maker.Ident(builderVariableName), rawName); + } else { + rhs = maker.Ident(fieldName); + } + JCExpression assign = maker.Assign(thisX, rhs); + assigns.append(maker.Exec(assign)); } @@ -300,6 +344,23 @@ public class HandleConstructor { } if (onConstructor != null) mods.annotations = mods.annotations.appendList(copyAnnotations(onConstructor)); + if (builderClassnameAsParameter != null) { + // Create a constructor that has just the builder as parameter. + params.clear(); + long flags = JavacHandlerUtil.addFinalIfNeeded(Flags.PARAMETER, typeNode.getContext()); + Name builderClassname = typeNode.toName(builderClassnameAsParameter); + JCVariableDecl param = maker.VarDef(maker.Modifiers(flags), builderVariableName, maker.Ident(builderClassname), null); + params.append(param); + } + + if (callBuilderBasedSuperConstructor) { + // The first statement must be the call to the super constructor. + JCMethodInvocation callToSuperConstructor = maker.Apply(List.<JCExpression>nil(), + maker.Ident(typeNode.toName("super")), + List.<JCExpression>of(maker.Ident(builderVariableName))); + assigns.prepend(maker.Exec(callToSuperConstructor)); + } + return recursiveSetGeneratedBy(maker.MethodDef(mods, typeNode.toName("<init>"), null, List.<JCTypeParameter>nil(), params.toList(), List.<JCExpression>nil(), maker.Block(0L, nullChecks.appendList(assigns).toList()), null), source.get(), typeNode.getContext()); |