diff options
-rw-r--r-- | doc/changelog.markdown | 3 | ||||
-rw-r--r-- | src/core/lombok/Builder.java | 33 | ||||
-rw-r--r-- | src/core/lombok/core/TypeLibrary.java | 38 | ||||
-rw-r--r-- | src/core/lombok/core/TypeResolver.java | 16 | ||||
-rw-r--r-- | src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java | 2 | ||||
-rw-r--r-- | src/core/lombok/eclipse/handlers/HandleBuilder.java | 266 | ||||
-rw-r--r-- | src/core/lombok/javac/handlers/HandleBuilder.java | 261 | ||||
-rw-r--r-- | test/transform/resource/after-delombok/BuilderWithToBuilder.java | 216 | ||||
-rw-r--r-- | test/transform/resource/after-ecj/BuilderWithToBuilder.java | 150 | ||||
-rw-r--r-- | test/transform/resource/before/BuilderWithToBuilder.java | 31 | ||||
-rw-r--r-- | website/features/Builder.html | 9 |
11 files changed, 919 insertions, 106 deletions
diff --git a/doc/changelog.markdown b/doc/changelog.markdown index 5f145f83..14baeb49 100644 --- a/doc/changelog.markdown +++ b/doc/changelog.markdown @@ -3,9 +3,10 @@ Lombok Changelog ### v1.16.5 "Edgy Guinea Pig" * FEATURE: `@Helper` can be placed on method-local inner classes to make all methods in the class accessible to the rest of the method. [Full documentation](https://projectlombok.org/features/experimental/Helper.html). +* FEATURE: `@Builder(toBuilder = true)` is now available. It produces an instance method that creates a new builder, initialized with all the values of that instance. For more, read the [Feature page on Builder](https://projectlombok.org/features/Builder.html). +* FEATURE: the `hashCode()` method generated by lombok via `@EqualsAndHashCode`, `@Data`, and `@Value` is now smarter about nulls; they are treated as if they hash to a magic prime instead of 0, which reduces hash collisions. * BUGFIX: Parameterized static methods with `@Builder` would produce compiler errors in javac. [Issue #828](https://github.com/rzwitserloot/lombok/issues/828). * PERFORMANCE: the config system caused significant slowdowns in eclipse if the filesystem is very slow (network file system) or has a slow authentication system. -* FEATURE: the `hashCode()` method generated by lombok via `@EqualsAndHashCode`, `@Data`, and `@Value` is now smarter about nulls; they are treated as if they hash to a magic prime instead of 0, which reduces hash collisions. * BUGFIX: Various quickfixes in Eclipse Mars were broken. [Issue #861](https://github.com/rzwitserloot/lombok/issues/861) [Issue #866](https://github.com/rzwitserloot/lombok/issues/866) [Issue #870](https://github.com/rzwitserloot/lombok/issues/870). ### v1.16.4 (April 14th, 2015) diff --git a/src/core/lombok/Builder.java b/src/core/lombok/Builder.java index 4556bef0..0639c4cb 100644 --- a/src/core/lombok/Builder.java +++ b/src/core/lombok/Builder.java @@ -113,9 +113,40 @@ public @interface Builder { /** Name of the instance method in the builder class that creates an instance of your {@code @Builder}-annotated class. */ String buildMethodName() default "build"; - /** Name of the builder class. + /** + * Name of the builder class. + * * Default for {@code @Builder} on types and constructors: {@code (TypeName)Builder}. + * <p> * Default for {@code @Builder} on static methods: {@code (ReturnTypeName)Builder}. */ String builderClassName() 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. + */ + boolean toBuilder() default false; + + /** + * Put on a field (in case of {@code @Builder} on a type) or a parameter (for {@code @Builder} on a constructor or static method) to + * indicate how lombok should obtain a value for this field or parameter given an instance; this is only relevant if {@code toBuilder} is {@code true}. + * + * You do not need to supply an {@code @ObtainVia} annotation unless you wish to change the default behaviour: Use a field with the same name. + * <p> + * Note that one of {@code field} or {@code method} should be set, or an error is generated. + * <p> + * The default behaviour is to obtain a value by referencing the name of the parameter as a field on 'this'. + */ + public @interface ObtainVia { + /** Tells lombok to obtain a value with the expression {@code this.value}. */ + String field() default ""; + + /** Tells lombok to obtain a value with the expression {@code this.method()}. */ + String method() default ""; + + /** Tells lombok to obtain a value with the expression {@code SelfType.method(this)}; requires {@code method} to be set. */ + boolean isStatic() default false; + } } diff --git a/src/core/lombok/core/TypeLibrary.java b/src/core/lombok/core/TypeLibrary.java index dc557c47..cdaf7a70 100644 --- a/src/core/lombok/core/TypeLibrary.java +++ b/src/core/lombok/core/TypeLibrary.java @@ -50,13 +50,20 @@ public class TypeLibrary { } private TypeLibrary(String fqnSingleton) { - unqualifiedToQualifiedMap = null; - qualified = fqnSingleton; - int idx = fqnSingleton.lastIndexOf('.'); - if (idx == -1) { - unqualified = fqnSingleton; + if (fqnSingleton.indexOf("$") != -1) { + unqualifiedToQualifiedMap = new HashMap<String, String>(); + unqualified = null; + qualified = null; + addType(fqnSingleton); } else { - unqualified = fqnSingleton.substring(idx + 1); + unqualifiedToQualifiedMap = null; + qualified = fqnSingleton; + int idx = fqnSingleton.lastIndexOf('.'); + if (idx == -1) { + unqualified = fqnSingleton; + } else { + unqualified = fqnSingleton.substring(idx + 1); + } } locked = true; } @@ -71,18 +78,29 @@ public class TypeLibrary { * @param fullyQualifiedTypeName the FQN type name, such as 'java.lang.String'. */ public void addType(String fullyQualifiedTypeName) { + String dotBased = fullyQualifiedTypeName.replace("$", "."); + if (locked) throw new IllegalStateException("locked"); - fullyQualifiedTypeName = fullyQualifiedTypeName.replace("$", "."); int idx = fullyQualifiedTypeName.lastIndexOf('.'); if (idx == -1) throw new IllegalArgumentException( "Only fully qualified types are allowed (and stuff in the default package is not palatable to us either!)"); String unqualified = fullyQualifiedTypeName.substring(idx + 1); if (unqualifiedToQualifiedMap == null) throw new IllegalStateException("SingleType library"); - unqualifiedToQualifiedMap.put(unqualified, fullyQualifiedTypeName); - unqualifiedToQualifiedMap.put(fullyQualifiedTypeName, fullyQualifiedTypeName); + unqualifiedToQualifiedMap.put(unqualified.replace("$", "."), dotBased); + unqualifiedToQualifiedMap.put(unqualified, dotBased); + unqualifiedToQualifiedMap.put(fullyQualifiedTypeName, dotBased); + unqualifiedToQualifiedMap.put(dotBased, dotBased); for (Map.Entry<String, String> e : LombokInternalAliasing.ALIASES.entrySet()) { - if (fullyQualifiedTypeName.equals(e.getValue())) unqualifiedToQualifiedMap.put(e.getKey(), fullyQualifiedTypeName); + if (fullyQualifiedTypeName.equals(e.getValue())) unqualifiedToQualifiedMap.put(e.getKey(), dotBased); + } + + int idx2 = fullyQualifiedTypeName.indexOf('$', idx + 1); + while (idx2 != -1) { + String unq = fullyQualifiedTypeName.substring(idx2 + 1); + unqualifiedToQualifiedMap.put(unq.replace("$", "."), dotBased); + unqualifiedToQualifiedMap.put(unq, dotBased); + idx2 = fullyQualifiedTypeName.indexOf('$', idx2 + 1); } } diff --git a/src/core/lombok/core/TypeResolver.java b/src/core/lombok/core/TypeResolver.java index 287a085f..60ac6b6a 100644 --- a/src/core/lombok/core/TypeResolver.java +++ b/src/core/lombok/core/TypeResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2013 The Project Lombok Authors. + * Copyright (C) 2009-2015 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -53,10 +53,13 @@ public class TypeResolver { if (typeRef.equals(qualified)) return typeRef; // When asking if 'Getter' could possibly be referring to 'lombok.Getter' if 'import lombok.Getter;' is in the source file, the answer is yes. - String fromExplicitImport = imports.getFullyQualifiedNameForSimpleName(typeRef); + int firstDot = typeRef.indexOf('.'); + if (firstDot == -1) firstDot = typeRef.length(); + String firstTypeRef = typeRef.substring(0, firstDot); + String fromExplicitImport = imports.getFullyQualifiedNameForSimpleName(firstTypeRef); if (fromExplicitImport != null) { // ... and if 'import foobar.Getter;' is in the source file, the answer is no. - return fromExplicitImport.equals(qualified) ? qualified : null; + return (fromExplicitImport + typeRef.substring(firstDot)).equals(qualified) ? qualified : null; } // When asking if 'Getter' could possibly be referring to 'lombok.Getter' and 'import lombok.*; / package lombok;' isn't in the source file. the answer is no. @@ -68,7 +71,7 @@ public class TypeResolver { mainLoop: while (n != null) { - if (n.getKind() == Kind.TYPE && typeRef.equals(n.getName())) { + if (n.getKind() == Kind.TYPE && firstTypeRef.equals(n.getName())) { // Our own class or one of our outer classes is named 'typeRef' so that's what 'typeRef' is referring to, not one of our type library classes. return null; } @@ -81,7 +84,7 @@ public class TypeResolver { for (LombokNode<?, ?, ?> child : newN.down()) { // We found a method local with the same name above our code. That's the one 'typeRef' is referring to, not // anything in the type library we're trying to find, so, no matches. - if (child.getKind() == Kind.TYPE && typeRef.equals(child.getName())) return null; + if (child.getKind() == Kind.TYPE && firstTypeRef.equals(child.getName())) return null; if (child == n) break; } } @@ -92,14 +95,13 @@ public class TypeResolver { if (n.getKind() == Kind.TYPE || n.getKind() == Kind.COMPILATION_UNIT) { for (LombokNode<?, ?, ?> child : n.down()) { // Inner class that's visible to us has 'typeRef' as name, so that's the one being referred to, not one of our type library classes. - if (child.getKind() == Kind.TYPE && typeRef.equals(child.getName())) return null; + if (child.getKind() == Kind.TYPE && firstTypeRef.equals(child.getName())) return null; } } n = n.directUp(); } - return qualified; } } diff --git a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java index 836a51c6..0ff5a7f6 100644 --- a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java +++ b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java @@ -450,7 +450,7 @@ public class EclipseHandlerUtil { */ public static boolean annotationTypeMatches(Class<? extends java.lang.annotation.Annotation> type, EclipseNode node) { if (node.getKind() != Kind.ANNOTATION) return false; - return typeMatches(type, node, ((Annotation)node.get()).type); + return typeMatches(type, node, ((Annotation) node.get()).type); } public static TypeReference cloneSelfType(EclipseNode context) { diff --git a/src/core/lombok/eclipse/handlers/HandleBuilder.java b/src/core/lombok/eclipse/handlers/HandleBuilder.java index c1b0d8a3..ee6dfd70 100644 --- a/src/core/lombok/eclipse/handlers/HandleBuilder.java +++ b/src/core/lombok/eclipse/handlers/HandleBuilder.java @@ -69,6 +69,7 @@ import org.mangosdk.spi.ProviderFor; import lombok.AccessLevel; import lombok.Builder; +import lombok.Builder.ObtainVia; import lombok.ConfigurationKeys; import lombok.Singular; import lombok.core.AST.Kind; @@ -98,12 +99,33 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { private static class BuilderFieldData { TypeReference type; + char[] rawName; char[] name; SingularData singularData; + ObtainVia obtainVia; + EclipseNode obtainViaNode; List<EclipseNode> createdFields = new ArrayList<EclipseNode>(); } + private static boolean equals(String a, char[] b) { + if (a.length() != b.length) return false; + for (int i = 0; i < b.length; i++) { + if (a.charAt(i) != b[i]) return false; + } + return true; + } + + private static boolean equals(String a, char[][] b) { + if (a == null || a.isEmpty()) return b.length == 0; + String[] aParts = a.split("\\."); + if (aParts.length != b.length) return false; + for (int i = 0; i < b.length; i++) { + if (!equals(aParts[i], b[i])) return false; + } + return true; + } + @Override public void handle(AnnotationValues<Builder> annotation, Annotation ast, EclipseNode annotationNode) { long p = (long) ast.sourceStart << 32 | ast.sourceEnd; @@ -116,6 +138,9 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { String builderMethodName = builderInstance.builderMethodName(); String buildMethodName = builderInstance.buildMethodName(); String builderClassName = builderInstance.builderClassName(); + String toBuilderMethodName = "toBuilder"; + boolean toBuilder = builderInstance.toBuilder(); + List<char[]> typeArgsForToBuilder = null; if (builderMethodName == null) builderMethodName = "builder"; if (buildMethodName == null) builderMethodName = "build"; @@ -153,9 +178,11 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { // Value will only skip making a field final if it has an explicit @NonFinal annotation, so we check for that. if (fd.initialization != null && valuePresent && !hasAnnotation(NonFinal.class, fieldNode)) continue; BuilderFieldData bfd = new BuilderFieldData(); + bfd.rawName = fieldNode.getName().toCharArray(); bfd.name = removePrefixFromField(fieldNode); bfd.type = fd.type; bfd.singularData = getSingularData(fieldNode, ast); + addObtainVia(bfd, fieldNode); builderFields.add(bfd); allFields.add(fieldNode); } @@ -189,6 +216,72 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { annotationNode.addError("@Builder is only supported on types, constructors, and static methods."); return; } + + if (toBuilder) { + final String TO_BUILDER_NOT_SUPPORTED = "@Builder(toBuilder=true) is only supported if you return your own type."; + char[] token; + char[][] pkg = null; + if (md.returnType.dimensions() > 0) { + annotationNode.addError(TO_BUILDER_NOT_SUPPORTED); + return; + } + + if (md.returnType instanceof SingleTypeReference) { + token = ((SingleTypeReference) md.returnType).token; + } else if (md.returnType instanceof QualifiedTypeReference) { + pkg = ((QualifiedTypeReference) md.returnType).tokens; + token = pkg[pkg.length]; + char[][] pkg_ = new char[pkg.length - 1][]; + System.arraycopy(pkg, 0, pkg_, 0, pkg_.length); + pkg = pkg_; + } else { + annotationNode.addError(TO_BUILDER_NOT_SUPPORTED); + return; + } + + if (pkg != null && !equals(parent.getPackageDeclaration(), pkg)) { + annotationNode.addError(TO_BUILDER_NOT_SUPPORTED); + return; + } + + if (tdParent == null || !equals(tdParent.getName(), token)) { + annotationNode.addError(TO_BUILDER_NOT_SUPPORTED); + return; + } + + TypeParameter[] tpOnType = ((TypeDeclaration) tdParent.get()).typeParameters; + TypeParameter[] tpOnMethod = md.typeParameters; + TypeReference[][] tpOnRet_ = md.returnType.getTypeArguments(); + + if (tpOnRet_ != null) for (int i = 0; i < tpOnRet_.length - 1; i++) { + if (tpOnRet_[i] != null && tpOnRet_[i].length > 0) { + annotationNode.addError("@Builder(toBuilder=true) is not supported if returning a type with generics applied to an intermediate."); + return; + } + } + TypeReference[] tpOnRet = tpOnRet_ == null ? null : tpOnRet_[tpOnRet_.length - 1]; + typeArgsForToBuilder = new ArrayList<char[]>(); + + // Every typearg on this method needs to be found in the return type, but the reverse is not true. + // We also need to 'map' them. + + + if (tpOnMethod != null) for (TypeParameter onMethod : tpOnMethod) { + int pos = -1; + if (tpOnRet != null) for (int i = 0; i < tpOnRet.length; i++) { + if (tpOnRet[i].getClass() != SingleTypeReference.class) continue; + if (!Arrays.equals(((SingleTypeReference) tpOnRet[i]).token, onMethod.name)) continue; + pos = i; + } + if (pos == -1 || tpOnType == null || tpOnType.length <= pos) { + annotationNode.addError("@Builder(toBuilder=true) requires that each type parameter on the static method is part of the typeargs of the return value. Type parameter " + new String(onMethod.name) + " is not part of the return type."); + return; + } + + typeArgsForToBuilder.add(tpOnType[pos].name); + } + } + returnType = copyType(md.returnType, ast); typeParams = md.typeParameters; thrownExceptions = md.thrownExceptions; @@ -232,9 +325,11 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { if (param.getKind() != Kind.ARGUMENT) continue; BuilderFieldData bfd = new BuilderFieldData(); Argument arg = (Argument) param.get(); + bfd.rawName = arg.name; bfd.name = arg.name; bfd.type = arg.type; bfd.singularData = getSingularData(param, ast); + addObtainVia(bfd, param); builderFields.add(bfd); } } @@ -264,6 +359,16 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { break; } } + if (bfd.obtainVia != null) { + if (bfd.obtainVia.field().isEmpty() == bfd.obtainVia.method().isEmpty()) { + bfd.obtainViaNode.addError("The syntax is either @ObtainVia(field = \"fieldName\") or @ObtainVia(method = \"methodName\")."); + return; + } + if (bfd.obtainVia.method().isEmpty() && bfd.obtainVia.isStatic()) { + bfd.obtainViaNode.addError("@ObtainVia(isStatic = true) is not valid unless 'method' has been set."); + return; + } + } } generateBuilderFields(builderType, builderFields, ast); @@ -272,14 +377,13 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { cleanDecl.declarationSourceEnd = -1; cleanDecl.modifiers = ClassFileConstants.AccPrivate; cleanDecl.type = TypeReference.baseTypeReference(TypeIds.T_boolean, 0); - System.out.println("INJECTING: cleaning"); injectFieldAndMarkGenerated(builderType, cleanDecl); } if (constructorExists(builderType) == MemberExistsResult.NOT_EXISTS) { ConstructorDeclaration cd = HandleConstructor.createConstructor( - AccessLevel.PACKAGE, builderType, Collections.<EclipseNode>emptyList(), null, - annotationNode, Collections.<Annotation>emptyList()); + AccessLevel.PACKAGE, builderType, Collections.<EclipseNode>emptyList(), null, + annotationNode, Collections.<Annotation>emptyList()); if (cd != null) injectMethod(builderType, cd); } @@ -310,6 +414,69 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { MethodDeclaration md = generateBuilderMethod(builderMethodName, builderClassName, tdParent, typeParams, ast); if (md != null) injectMethod(tdParent, md); } + + if (toBuilder) switch (methodExists(toBuilderMethodName, tdParent, 0)) { + case EXISTS_BY_USER: + annotationNode.addWarning("Not generating toBuilder() as it already exists."); + break; + case NOT_EXISTS: + TypeParameter[] tps = typeParams; + if (typeArgsForToBuilder != null) { + tps = new TypeParameter[typeArgsForToBuilder.size()]; + for (int i = 0; i < tps.length; i++) { + tps[i] = new TypeParameter(); + tps[i].name = typeArgsForToBuilder.get(i); + } + } + MethodDeclaration md = generateToBuilderMethod(toBuilderMethodName, builderClassName, tdParent, tps, builderFields, fluent, ast); + + if (md != null) injectMethod(tdParent, md); + } + } + + private MethodDeclaration generateToBuilderMethod(String methodName, String builderClassName, EclipseNode type, TypeParameter[] typeParams, List<BuilderFieldData> builderFields, boolean fluent, ASTNode source) { + // return new ThingieBuilder<A, B>().setA(this.a).setB(this.b); + + int pS = source.sourceStart, pE = source.sourceEnd; + long p = (long) pS << 32 | pE; + + MethodDeclaration out = new MethodDeclaration( + ((CompilationUnitDeclaration) type.top().get()).compilationResult); + out.selector = methodName.toCharArray(); + out.modifiers = ClassFileConstants.AccPublic; + out.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG; + out.returnType = namePlusTypeParamsToTypeReference(builderClassName.toCharArray(), typeParams, p); + AllocationExpression invoke = new AllocationExpression(); + invoke.type = namePlusTypeParamsToTypeReference(builderClassName.toCharArray(), typeParams, p); + + Expression receiver = invoke; + for (BuilderFieldData bfd : builderFields) { + char[] setterName = fluent ? bfd.name : HandlerUtil.buildAccessorName("set", new String(bfd.name)).toCharArray(); + MessageSend ms = new MessageSend(); + if (bfd.obtainVia == null || !bfd.obtainVia.field().isEmpty()) { + char[] fieldName = bfd.obtainVia == null ? bfd.rawName : bfd.obtainVia.field().toCharArray(); + FieldReference fr = new FieldReference(fieldName, 0); + fr.receiver = new ThisReference(0, 0); + ms.arguments = new Expression[] {fr}; + } else { + String obtainName = bfd.obtainVia.method(); + boolean obtainIsStatic = bfd.obtainVia.isStatic(); + MessageSend obtainExpr = new MessageSend(); + obtainExpr.receiver = obtainIsStatic ? new SingleNameReference(type.getName().toCharArray(), 0) : new ThisReference(0, 0); + obtainExpr.selector = obtainName.toCharArray(); + if (obtainIsStatic) obtainExpr.arguments = new Expression[] {new ThisReference(0, 0)}; + ms.arguments = new Expression[] {obtainExpr}; + } + ms.receiver = receiver; + ms.selector = setterName; + receiver = ms; + } + + out.statements = new Statement[] {new ReturnStatement(receiver, pS, pE)}; + + out.traverse(new SetGeneratedByVisitor(source), ((TypeDeclaration) type.get()).scope); + return out; + } private MethodDeclaration generateCleanMethod(List<BuilderFieldData> builderFields, EclipseNode builderType, ASTNode source) { @@ -501,6 +668,16 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { return injectType(tdParent, builder); } + private void addObtainVia(BuilderFieldData bfd, EclipseNode node) { + for (EclipseNode child : node.down()) { + if (!annotationTypeMatches(ObtainVia.class, child)) continue; + AnnotationValues<ObtainVia> ann = createAnnotation(ObtainVia.class, child); + bfd.obtainVia = ann.getInstance(); + bfd.obtainViaNode = child; + return; + } + } + /** * Returns the explicitly requested singular annotation on this node (field * or parameter), or null if there's no {@code @Singular} annotation on it. @@ -509,53 +686,52 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> { */ private SingularData getSingularData(EclipseNode node, ASTNode source) { for (EclipseNode child : node.down()) { - if (child.getKind() == Kind.ANNOTATION && annotationTypeMatches(Singular.class, child)) { - char[] pluralName = node.getKind() == Kind.FIELD ? removePrefixFromField(node) : ((AbstractVariableDeclaration) node.get()).name; - AnnotationValues<Singular> ann = createAnnotation(Singular.class, child); - String explicitSingular = ann.getInstance().value(); - if (explicitSingular.isEmpty()) { - if (Boolean.FALSE.equals(node.getAst().readConfiguration(ConfigurationKeys.SINGULAR_AUTO))) { - node.addError("The singular must be specified explicitly (e.g. @Singular(\"task\")) because auto singularization is disabled."); + if (!annotationTypeMatches(Singular.class, child)) continue; + char[] pluralName = node.getKind() == Kind.FIELD ? removePrefixFromField(node) : ((AbstractVariableDeclaration) node.get()).name; + AnnotationValues<Singular> ann = createAnnotation(Singular.class, child); + String explicitSingular = ann.getInstance().value(); + if (explicitSingular.isEmpty()) { + if (Boolean.FALSE.equals(node.getAst().readConfiguration(ConfigurationKeys.SINGULAR_AUTO))) { + node.addError("The singular must be specified explicitly (e.g. @Singular(\"task\")) because auto singularization is disabled."); + explicitSingular = new String(pluralName); + } else { + explicitSingular = autoSingularize(node.getName()); + if (explicitSingular == null) { + node.addError("Can't singularize this name; please specify the singular explicitly (i.e. @Singular(\"sheep\"))"); explicitSingular = new String(pluralName); - } else { - explicitSingular = autoSingularize(node.getName()); - if (explicitSingular == null) { - node.addError("Can't singularize this name; please specify the singular explicitly (i.e. @Singular(\"sheep\"))"); - explicitSingular = new String(pluralName); - } } } - char[] singularName = explicitSingular.toCharArray(); - - TypeReference type = ((AbstractVariableDeclaration) node.get()).type; - TypeReference[] typeArgs = null; - String typeName; - if (type instanceof ParameterizedSingleTypeReference) { - typeArgs = ((ParameterizedSingleTypeReference) type).typeArguments; - typeName = new String(((ParameterizedSingleTypeReference) type).token); - } else if (type instanceof ParameterizedQualifiedTypeReference) { - TypeReference[][] tr = ((ParameterizedQualifiedTypeReference) type).typeArguments; - if (tr != null) typeArgs = tr[tr.length - 1]; - char[][] tokens = ((ParameterizedQualifiedTypeReference) type).tokens; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < tokens.length; i++) { - if (i > 0) sb.append("."); - sb.append(tokens[i]); - } - typeName = sb.toString(); - } else { - typeName = type.toString(); - } - - String targetFqn = EclipseSingularsRecipes.get().toQualified(typeName); - EclipseSingularizer singularizer = EclipseSingularsRecipes.get().getSingularizer(targetFqn); - if (singularizer == null) { - node.addError("Lombok does not know how to create the singular-form builder methods for type '" + typeName + "'; they won't be generated."); - return null; + } + char[] singularName = explicitSingular.toCharArray(); + + TypeReference type = ((AbstractVariableDeclaration) node.get()).type; + TypeReference[] typeArgs = null; + String typeName; + if (type instanceof ParameterizedSingleTypeReference) { + typeArgs = ((ParameterizedSingleTypeReference) type).typeArguments; + typeName = new String(((ParameterizedSingleTypeReference) type).token); + } else if (type instanceof ParameterizedQualifiedTypeReference) { + TypeReference[][] tr = ((ParameterizedQualifiedTypeReference) type).typeArguments; + if (tr != null) typeArgs = tr[tr.length - 1]; + char[][] tokens = ((ParameterizedQualifiedTypeReference) type).tokens; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) { + if (i > 0) sb.append("."); + sb.append(tokens[i]); } - - return new SingularData(child, singularName, pluralName, typeArgs == null ? Collections.<TypeReference>emptyList() : Arrays.asList(typeArgs), targetFqn, singularizer, source); + typeName = sb.toString(); + } else { + typeName = type.toString(); } + + String targetFqn = EclipseSingularsRecipes.get().toQualified(typeName); + EclipseSingularizer singularizer = EclipseSingularsRecipes.get().getSingularizer(targetFqn); + if (singularizer == null) { + node.addError("Lombok does not know how to create the singular-form builder methods for type '" + typeName + "'; they won't be generated."); + return null; + } + + return new SingularData(child, singularName, pluralName, typeArgs == null ? Collections.<TypeReference>emptyList() : Arrays.asList(typeArgs), targetFqn, singularizer, source); } return null; diff --git a/src/core/lombok/javac/handlers/HandleBuilder.java b/src/core/lombok/javac/handlers/HandleBuilder.java index 7ef7b859..484a0b25 100644 --- a/src/core/lombok/javac/handlers/HandleBuilder.java +++ b/src/core/lombok/javac/handlers/HandleBuilder.java @@ -29,6 +29,7 @@ import org.mangosdk.spi.ProviderFor; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCAnnotation; +import com.sun.tools.javac.tree.JCTree.JCArrayTypeTree; import com.sun.tools.javac.tree.JCTree.JCBlock; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCExpression; @@ -49,6 +50,7 @@ import com.sun.tools.javac.util.Name; import lombok.AccessLevel; import lombok.Builder; +import lombok.Builder.ObtainVia; import lombok.ConfigurationKeys; import lombok.Singular; import lombok.core.AST.Kind; @@ -79,8 +81,11 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { private static class BuilderFieldData { JCExpression type; + Name rawName; Name name; SingularData singularData; + ObtainVia obtainVia; + JavacNode obtainViaNode; java.util.List<JavacNode> createdFields = new ArrayList<JavacNode>(); } @@ -95,6 +100,9 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { String builderMethodName = builderInstance.builderMethodName(); String buildMethodName = builderInstance.buildMethodName(); String builderClassName = builderInstance.builderClassName(); + String toBuilderMethodName = "toBuilder"; + boolean toBuilder = builderInstance.toBuilder(); + java.util.List<Name> typeArgsForToBuilder = null; if (builderMethodName == null) builderMethodName = "builder"; if (buildMethodName == null) buildMethodName = "build"; @@ -135,9 +143,11 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { // Value will only skip making a field final if it has an explicit @NonFinal annotation, so we check for that. if (fd.init != null && valuePresent && !hasAnnotation(NonFinal.class, fieldNode)) continue; BuilderFieldData bfd = new BuilderFieldData(); + bfd.rawName = fd.name; bfd.name = removePrefixFromField(fieldNode); bfd.type = fd.vartype; bfd.singularData = getSingularData(fieldNode); + addObtainVia(bfd, fieldNode); builderFields.add(bfd); allFields.append(fieldNode); } @@ -171,7 +181,8 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { annotationNode.addError("@Builder is only supported on types, constructors, and static methods."); return; } - returnType = jmd.restype; + JCExpression fullReturnType = jmd.restype; + returnType = fullReturnType; typeParams = jmd.typarams; thrownExceptions = jmd.thrown; nameOfStaticBuilderMethod = jmd.name; @@ -202,6 +213,70 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { builderClassName = td.name.toString() + "Builder"; } } + if (toBuilder) { + final String TO_BUILDER_NOT_SUPPORTED = "@Builder(toBuilder=true) is only supported if you return your own type."; + if (returnType instanceof JCArrayTypeTree) { + annotationNode.addError(TO_BUILDER_NOT_SUPPORTED); + return; + } + + Name simpleName; + String pkg; + List<JCExpression> tpOnRet = List.nil(); + + if (fullReturnType instanceof JCTypeApply) { + tpOnRet = ((JCTypeApply) fullReturnType).arguments; + } + + if (returnType instanceof JCIdent) { + simpleName = ((JCIdent) returnType).name; + pkg = null; + } else if (returnType instanceof JCFieldAccess) { + JCFieldAccess jcfa = (JCFieldAccess) returnType; + simpleName = jcfa.name; + pkg = unpack(jcfa.selected); + if (pkg.startsWith("ERR:")) { + String err = pkg.substring(4, pkg.indexOf("__ERR__")); + annotationNode.addError(err); + return; + } + } else { + annotationNode.addError("Expected a (parameterized) type here instead of a " + returnType.getClass().getName()); + return; + } + + if (pkg != null && !parent.getPackageDeclaration().equals(pkg)) { + annotationNode.addError(TO_BUILDER_NOT_SUPPORTED); + return; + } + + if (!tdParent.getName().contentEquals(simpleName)) { + annotationNode.addError(TO_BUILDER_NOT_SUPPORTED); + return; + } + + List<JCTypeParameter> tpOnMethod = jmd.typarams; + List<JCTypeParameter> tpOnType = ((JCClassDecl) tdParent.get()).typarams; + typeArgsForToBuilder = new ArrayList<Name>(); + + for (JCTypeParameter tp : tpOnMethod) { + int pos = -1; + int idx = -1; + for (JCExpression tOnRet : tpOnRet) { + idx++; + if (!(tOnRet instanceof JCIdent)) continue; + if (((JCIdent) tOnRet).name != tp.name) continue; + pos = idx; + } + + if (pos == -1 || tpOnType.size() <= pos) { + annotationNode.addError("**" + returnType.getClass().toString()); +// annotationNode.addError("@Builder(toBuilder=true) requires that each type parameter on the static method is part of the typeargs of the return value. Type parameter " + tp.name + " is not part of the return type."); + return; + } + typeArgsForToBuilder.add(tpOnType.get(pos).name); + } + } } else { annotationNode.addError("@Builder is only supported on types, constructors, and static methods."); return; @@ -213,8 +288,10 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { BuilderFieldData bfd = new BuilderFieldData(); JCVariableDecl raw = (JCVariableDecl) param.get(); bfd.name = raw.name; + bfd.rawName = raw.name; bfd.type = raw.vartype; bfd.singularData = getSingularData(param); + addObtainVia(bfd, param); builderFields.add(bfd); } } @@ -245,6 +322,16 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { break; } } + if (bfd.obtainVia != null) { + if (bfd.obtainVia.field().isEmpty() == bfd.obtainVia.method().isEmpty()) { + bfd.obtainViaNode.addError("The syntax is either @ObtainVia(field = \"fieldName\") or @ObtainVia(method = \"methodName\")."); + return; + } + if (bfd.obtainVia.method().isEmpty() && bfd.obtainVia.isStatic()) { + bfd.obtainViaNode.addError("@ObtainVia(isStatic = true) is not valid unless 'method' has been set."); + return; + } + } } generateBuilderFields(builderType, builderFields, ast); @@ -285,9 +372,95 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { if (md != null) injectMethod(tdParent, md); } + if (toBuilder) { + switch (methodExists(toBuilderMethodName, tdParent, 0)) { + case EXISTS_BY_USER: + annotationNode.addWarning("Not generating toBuilder() as it already exists."); + return; + case NOT_EXISTS: + List<JCTypeParameter> tps = typeParams; + if (typeArgsForToBuilder != null) { + ListBuffer<JCTypeParameter> lb = new ListBuffer<JCTypeParameter>(); + JavacTreeMaker maker = tdParent.getTreeMaker(); + for (Name n : typeArgsForToBuilder) { + lb.append(maker.TypeParameter(n, List.<JCExpression>nil())); + } + tps = lb.toList(); + } + JCMethodDecl md = generateToBuilderMethod(toBuilderMethodName, builderClassName, tdParent, tps, builderFields, fluent, ast); + if (md != null) injectMethod(tdParent, md); + } + } + recursiveSetGeneratedBy(builderType.get(), ast, annotationNode.getContext()); } + private static String unpack(JCExpression expr) { + StringBuilder sb = new StringBuilder(); + unpack(sb, expr); + return sb.toString(); + } + + private static void unpack(StringBuilder sb, JCExpression expr) { + if (expr instanceof JCIdent) { + sb.append(((JCIdent) expr).name.toString()); + return; + } + + if (expr instanceof JCFieldAccess) { + JCFieldAccess jcfa = (JCFieldAccess) expr; + unpack(sb, jcfa.selected); + sb.append(".").append(jcfa.name.toString()); + return; + } + + if (expr instanceof JCTypeApply) { + sb.setLength(0); + sb.append("ERR:"); + sb.append("@Builder(toBuilder=true) is not supported if returning a type with generics applied to an intermediate."); + sb.append("__ERR__"); + return; + } + + sb.setLength(0); + sb.append("ERR:"); + sb.append("Expected a type of some sort, not a " + expr.getClass().getName()); + sb.append("__ERR__"); + } + + private JCMethodDecl generateToBuilderMethod(String toBuilderMethodName, String builderClassName, JavacNode type, List<JCTypeParameter> typeParams, java.util.List<BuilderFieldData> builderFields, boolean fluent, JCAnnotation ast) { + // return new ThingieBuilder<A, B>().setA(this.a).setB(this.b); + JavacTreeMaker maker = type.getTreeMaker(); + + ListBuffer<JCExpression> typeArgs = new ListBuffer<JCExpression>(); + for (JCTypeParameter typeParam : typeParams) { + typeArgs.append(maker.Ident(typeParam.name)); + } + + JCExpression call = maker.NewClass(null, List.<JCExpression>nil(), namePlusTypeParamsToTypeReference(maker, type.toName(builderClassName), typeParams), List.<JCExpression>nil(), null); + JCExpression invoke = call; + for (BuilderFieldData bfd : builderFields) { + Name setterName = fluent ? bfd.name : type.toName(HandlerUtil.buildAccessorName("set", bfd.name.toString())); + JCExpression arg; + if (bfd.obtainVia == null || !bfd.obtainVia.field().isEmpty()) { + arg = maker.Select(maker.Ident(type.toName("this")), bfd.obtainVia == null ? bfd.rawName : type.toName(bfd.obtainVia.field())); + } else { + if (bfd.obtainVia.isStatic()) { + JCExpression c = maker.Select(maker.Ident(type.toName(type.getName())), type.toName(bfd.obtainVia.method())); + arg = maker.Apply(List.<JCExpression>nil(), c, List.<JCExpression>of(maker.Ident(type.toName("this")))); + } else { + JCExpression c = maker.Select(maker.Ident(type.toName("this")), type.toName(bfd.obtainVia.method())); + arg = maker.Apply(List.<JCExpression>nil(), c, List.<JCExpression>nil()); + } + } + invoke = maker.Apply(List.<JCExpression>nil(), maker.Select(invoke, setterName), List.of(arg)); + } + JCStatement statement = maker.Return(invoke); + + JCBlock body = maker.Block(0, List.<JCStatement>of(statement)); + return maker.MethodDef(maker.Modifiers(Flags.PUBLIC), type.toName(toBuilderMethodName), namePlusTypeParamsToTypeReference(maker, type.toName(builderClassName), typeParams), List.<JCTypeParameter>nil(), List.<JCVariableDecl>nil(), List.<JCExpression>nil(), body, null); + } + private JCMethodDecl generateCleanMethod(java.util.List<BuilderFieldData> builderFields, JavacNode type, JCTree source) { JavacTreeMaker maker = type.getTreeMaker(); ListBuffer<JCStatement> statements = new ListBuffer<JCStatement>(); @@ -449,6 +622,17 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { return injectType(tdParent, builder); } + private void addObtainVia(BuilderFieldData bfd, JavacNode node) { + for (JavacNode child : node.down()) { + if (!annotationTypeMatches(ObtainVia.class, child)) continue; + AnnotationValues<ObtainVia> ann = createAnnotation(ObtainVia.class, child); + bfd.obtainVia = ann.getInstance(); + bfd.obtainViaNode = child; + deleteAnnotationIfNeccessary(child, ObtainVia.class); + return; + } + } + /** * Returns the explicitly requested singular annotation on this node (field * or parameter), or null if there's no {@code @Singular} annotation on it. @@ -457,48 +641,47 @@ public class HandleBuilder extends JavacAnnotationHandler<Builder> { */ private SingularData getSingularData(JavacNode node) { for (JavacNode child : node.down()) { - if (child.getKind() == Kind.ANNOTATION && annotationTypeMatches(Singular.class, child)) { - Name pluralName = node.getKind() == Kind.FIELD ? removePrefixFromField(node) : ((JCVariableDecl) node.get()).name; - AnnotationValues<Singular> ann = createAnnotation(Singular.class, child); - deleteAnnotationIfNeccessary(child, Singular.class); - String explicitSingular = ann.getInstance().value(); - if (explicitSingular.isEmpty()) { - if (Boolean.FALSE.equals(node.getAst().readConfiguration(ConfigurationKeys.SINGULAR_AUTO))) { - node.addError("The singular must be specified explicitly (e.g. @Singular(\"task\")) because auto singularization is disabled."); + if (!annotationTypeMatches(Singular.class, child)) continue; + Name pluralName = node.getKind() == Kind.FIELD ? removePrefixFromField(node) : ((JCVariableDecl) node.get()).name; + AnnotationValues<Singular> ann = createAnnotation(Singular.class, child); + deleteAnnotationIfNeccessary(child, Singular.class); + String explicitSingular = ann.getInstance().value(); + if (explicitSingular.isEmpty()) { + if (Boolean.FALSE.equals(node.getAst().readConfiguration(ConfigurationKeys.SINGULAR_AUTO))) { + node.addError("The singular must be specified explicitly (e.g. @Singular(\"task\")) because auto singularization is disabled."); + explicitSingular = pluralName.toString(); + } else { + explicitSingular = autoSingularize(node.getName()); + if (explicitSingular == null) { + node.addError("Can't singularize this name; please specify the singular explicitly (i.e. @Singular(\"sheep\"))"); explicitSingular = pluralName.toString(); - } else { - explicitSingular = autoSingularize(node.getName()); - if (explicitSingular == null) { - node.addError("Can't singularize this name; please specify the singular explicitly (i.e. @Singular(\"sheep\"))"); - explicitSingular = pluralName.toString(); - } } } - Name singularName = node.toName(explicitSingular); - - JCExpression type = null; - if (node.get() instanceof JCVariableDecl) { - type = ((JCVariableDecl) node.get()).vartype; - } - - String name = null; - List<JCExpression> typeArgs = List.nil(); - if (type instanceof JCTypeApply) { - typeArgs = ((JCTypeApply) type).arguments; - type = ((JCTypeApply) type).clazz; - } - - name = type.toString(); - - String targetFqn = JavacSingularsRecipes.get().toQualified(name); - JavacSingularizer singularizer = JavacSingularsRecipes.get().getSingularizer(targetFqn); - if (singularizer == null) { - node.addError("Lombok does not know how to create the singular-form builder methods for type '" + name + "'; they won't be generated."); - return null; - } - - return new SingularData(child, singularName, pluralName, typeArgs, targetFqn, singularizer); } + Name singularName = node.toName(explicitSingular); + + JCExpression type = null; + if (node.get() instanceof JCVariableDecl) { + type = ((JCVariableDecl) node.get()).vartype; + } + + String name = null; + List<JCExpression> typeArgs = List.nil(); + if (type instanceof JCTypeApply) { + typeArgs = ((JCTypeApply) type).arguments; + type = ((JCTypeApply) type).clazz; + } + + name = type.toString(); + + String targetFqn = JavacSingularsRecipes.get().toQualified(name); + JavacSingularizer singularizer = JavacSingularsRecipes.get().getSingularizer(targetFqn); + if (singularizer == null) { + node.addError("Lombok does not know how to create the singular-form builder methods for type '" + name + "'; they won't be generated."); + return null; + } + + return new SingularData(child, singularName, pluralName, typeArgs, targetFqn, singularizer); } return null; diff --git a/test/transform/resource/after-delombok/BuilderWithToBuilder.java b/test/transform/resource/after-delombok/BuilderWithToBuilder.java new file mode 100644 index 00000000..eb61a6db --- /dev/null +++ b/test/transform/resource/after-delombok/BuilderWithToBuilder.java @@ -0,0 +1,216 @@ +import java.util.List; +class BuilderWithToBuilder<T> { + private String mOne; + private String mTwo; + private T foo; + private List<T> bars; + public static <K> K rrr(BuilderWithToBuilder<K> x) { + return x.foo; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + BuilderWithToBuilder(final String one, final String two, final T foo, final List<T> bars) { + this.mOne = one; + this.mTwo = two; + this.foo = foo; + this.bars = bars; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public static class BuilderWithToBuilderBuilder<T> { + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private String one; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private String two; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private T foo; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private java.util.ArrayList<T> bars; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + BuilderWithToBuilderBuilder() { + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public BuilderWithToBuilderBuilder<T> one(final String one) { + this.one = one; + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public BuilderWithToBuilderBuilder<T> two(final String two) { + this.two = two; + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public BuilderWithToBuilderBuilder<T> foo(final T foo) { + this.foo = foo; + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public BuilderWithToBuilderBuilder<T> bar(final T bar) { + if (this.bars == null) this.bars = new java.util.ArrayList<T>(); + this.bars.add(bar); + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public BuilderWithToBuilderBuilder<T> bars(final java.util.Collection<? extends T> bars) { + if (this.bars == null) this.bars = new java.util.ArrayList<T>(); + this.bars.addAll(bars); + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public BuilderWithToBuilder<T> build() { + java.util.List<T> bars; + switch (this.bars == null ? 0 : this.bars.size()) { + case 0: + bars = java.util.Collections.emptyList(); + break; + case 1: + bars = java.util.Collections.singletonList(this.bars.get(0)); + break; + default: + bars = java.util.Collections.unmodifiableList(new java.util.ArrayList<T>(this.bars)); + } + return new BuilderWithToBuilder<T>(one, two, foo, bars); + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public java.lang.String toString() { + return "BuilderWithToBuilder.BuilderWithToBuilderBuilder(one=" + this.one + ", two=" + this.two + ", foo=" + this.foo + ", bars=" + this.bars + ")"; + } + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public static <T> BuilderWithToBuilderBuilder<T> builder() { + return new BuilderWithToBuilderBuilder<T>(); + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public BuilderWithToBuilderBuilder<T> toBuilder() { + return new BuilderWithToBuilderBuilder<T>().one(this.mOne).two(this.mTwo).foo(BuilderWithToBuilder.rrr(this)).bars(this.bars); + } +} +class ConstructorWithToBuilder<T> { + private String mOne; + private String mTwo; + private T foo; + @lombok.Singular + private List<T> bars; + public ConstructorWithToBuilder(String mOne, T bar) { + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public static class ConstructorWithToBuilderBuilder<T> { + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private String mOne; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private T bar; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + ConstructorWithToBuilderBuilder() { + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public ConstructorWithToBuilderBuilder<T> mOne(final String mOne) { + this.mOne = mOne; + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public ConstructorWithToBuilderBuilder<T> bar(final T bar) { + this.bar = bar; + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public ConstructorWithToBuilder<T> build() { + return new ConstructorWithToBuilder<T>(mOne, bar); + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public java.lang.String toString() { + return "ConstructorWithToBuilder.ConstructorWithToBuilderBuilder(mOne=" + this.mOne + ", bar=" + this.bar + ")"; + } + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public static <T> ConstructorWithToBuilderBuilder<T> builder() { + return new ConstructorWithToBuilderBuilder<T>(); + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public ConstructorWithToBuilderBuilder<T> toBuilder() { + return new ConstructorWithToBuilderBuilder<T>().mOne(this.mOne).bar(this.foo); + } +} +class StaticWithToBuilder<T, K> { + private String mOne; + private String mTwo; + private T foo; + private K bar; + @lombok.Singular + private List<T> bars; + public static <Z> StaticWithToBuilder<Z, String> test(String mOne, Z bar) { + return new StaticWithToBuilder<Z, String>(); + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public static class StaticWithToBuilderBuilder<Z> { + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private String mOne; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + private Z bar; + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + StaticWithToBuilderBuilder() { + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public StaticWithToBuilderBuilder<Z> mOne(final String mOne) { + this.mOne = mOne; + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public StaticWithToBuilderBuilder<Z> bar(final Z bar) { + this.bar = bar; + return this; + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public StaticWithToBuilder build() { + return StaticWithToBuilder.<Z>test(mOne, bar); + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public java.lang.String toString() { + return "StaticWithToBuilder.StaticWithToBuilderBuilder(mOne=" + this.mOne + ", bar=" + this.bar + ")"; + } + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public static <Z> StaticWithToBuilderBuilder<Z> builder() { + return new StaticWithToBuilderBuilder<Z>(); + } + @java.lang.SuppressWarnings("all") + @javax.annotation.Generated("lombok") + public StaticWithToBuilderBuilder<T> toBuilder() { + return new StaticWithToBuilderBuilder<T>().mOne(this.mOne).bar(this.foo); + } +} diff --git a/test/transform/resource/after-ecj/BuilderWithToBuilder.java b/test/transform/resource/after-ecj/BuilderWithToBuilder.java new file mode 100644 index 00000000..423865ff --- /dev/null +++ b/test/transform/resource/after-ecj/BuilderWithToBuilder.java @@ -0,0 +1,150 @@ +import java.util.List; +import lombok.Builder; +@Builder(toBuilder = true) @lombok.experimental.Accessors(prefix = "m") class BuilderWithToBuilder<T> { + public static @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") class BuilderWithToBuilderBuilder<T> { + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") String one; + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") String two; + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") T foo; + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") java.util.ArrayList<T> bars; + @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilderBuilder() { + super(); + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilderBuilder<T> one(final String one) { + this.one = one; + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilderBuilder<T> two(final String two) { + this.two = two; + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilderBuilder<T> foo(final T foo) { + this.foo = foo; + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilderBuilder<T> bar(T bar) { + if ((this.bars == null)) + this.bars = new java.util.ArrayList<T>(); + this.bars.add(bar); + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilderBuilder<T> bars(java.util.Collection<? extends T> bars) { + if ((this.bars == null)) + this.bars = new java.util.ArrayList<T>(); + this.bars.addAll(bars); + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilder<T> build() { + java.util.List<T> bars; + switch (((this.bars == null) ? 0 : this.bars.size())) { + case 0 : + bars = java.util.Collections.emptyList(); + break; + case 1 : + bars = java.util.Collections.singletonList(this.bars.get(0)); + break; + default : + bars = java.util.Collections.unmodifiableList(new java.util.ArrayList<T>(this.bars)); + } + return new BuilderWithToBuilder<T>(one, two, foo, bars); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") java.lang.String toString() { + return (((((((("BuilderWithToBuilder.BuilderWithToBuilderBuilder(one=" + this.one) + ", two=") + this.two) + ", foo=") + this.foo) + ", bars=") + this.bars) + ")"); + } + } + private String mOne; + private String mTwo; + private @Builder.ObtainVia(method = "rrr",isStatic = true) T foo; + private @lombok.Singular List<T> bars; + public static <K>K rrr(BuilderWithToBuilder<K> x) { + return x.foo; + } + @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilder(final String one, final String two, final T foo, final List<T> bars) { + super(); + this.mOne = one; + this.mTwo = two; + this.foo = foo; + this.bars = bars; + } + public static @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") <T>BuilderWithToBuilderBuilder<T> builder() { + return new BuilderWithToBuilderBuilder<T>(); + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") BuilderWithToBuilderBuilder<T> toBuilder() { + return new BuilderWithToBuilderBuilder<T>().one(this.mOne).two(this.mTwo).foo(BuilderWithToBuilder.rrr(this)).bars(this.bars); + } +} +@lombok.experimental.Accessors(prefix = "m") class ConstructorWithToBuilder<T> { + public static @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") class ConstructorWithToBuilderBuilder<T> { + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") String mOne; + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") T bar; + @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") ConstructorWithToBuilderBuilder() { + super(); + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") ConstructorWithToBuilderBuilder<T> mOne(final String mOne) { + this.mOne = mOne; + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") ConstructorWithToBuilderBuilder<T> bar(final T bar) { + this.bar = bar; + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") ConstructorWithToBuilder<T> build() { + return new ConstructorWithToBuilder<T>(mOne, bar); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") java.lang.String toString() { + return (((("ConstructorWithToBuilder.ConstructorWithToBuilderBuilder(mOne=" + this.mOne) + ", bar=") + this.bar) + ")"); + } + } + private String mOne; + private String mTwo; + private T foo; + private @lombok.Singular List<T> bars; + public @Builder(toBuilder = true) ConstructorWithToBuilder(String mOne, @Builder.ObtainVia(field = "foo") T bar) { + super(); + } + public static @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") <T>ConstructorWithToBuilderBuilder<T> builder() { + return new ConstructorWithToBuilderBuilder<T>(); + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") ConstructorWithToBuilderBuilder<T> toBuilder() { + return new ConstructorWithToBuilderBuilder<T>().mOne(this.mOne).bar(this.foo); + } +} +@lombok.experimental.Accessors(prefix = "m") class StaticWithToBuilder<T, K> { + public static @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") class StaticWithToBuilderBuilder<Z> { + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") String mOne; + private @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") Z bar; + @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") StaticWithToBuilderBuilder() { + super(); + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") StaticWithToBuilderBuilder<Z> mOne(final String mOne) { + this.mOne = mOne; + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") StaticWithToBuilderBuilder<Z> bar(final Z bar) { + this.bar = bar; + return this; + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") StaticWithToBuilder<Z, String> build() { + return StaticWithToBuilder.<Z>test(mOne, bar); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") java.lang.String toString() { + return (((("StaticWithToBuilder.StaticWithToBuilderBuilder(mOne=" + this.mOne) + ", bar=") + this.bar) + ")"); + } + } + private String mOne; + private String mTwo; + private T foo; + private K bar; + private @lombok.Singular List<T> bars; + StaticWithToBuilder() { + super(); + } + public static @Builder(toBuilder = true) <Z>StaticWithToBuilder<Z, String> test(String mOne, @Builder.ObtainVia(field = "foo") Z bar) { + return new StaticWithToBuilder<Z, String>(); + } + public static @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") <Z>StaticWithToBuilderBuilder<Z> builder() { + return new StaticWithToBuilderBuilder<Z>(); + } + public @java.lang.SuppressWarnings("all") @javax.annotation.Generated("lombok") StaticWithToBuilderBuilder<T> toBuilder() { + return new StaticWithToBuilderBuilder<T>().mOne(this.mOne).bar(this.foo); + } +} diff --git a/test/transform/resource/before/BuilderWithToBuilder.java b/test/transform/resource/before/BuilderWithToBuilder.java new file mode 100644 index 00000000..63e16ae8 --- /dev/null +++ b/test/transform/resource/before/BuilderWithToBuilder.java @@ -0,0 +1,31 @@ +import java.util.List; +import lombok.Builder; +@Builder(toBuilder = true) @lombok.experimental.Accessors(prefix = "m") +class BuilderWithToBuilder<T> { + private String mOne, mTwo; + @Builder.ObtainVia(method = "rrr", isStatic = true) private T foo; + @lombok.Singular private List<T> bars; + public static <K> K rrr(BuilderWithToBuilder<K> x) { + return x.foo; + } +} +@lombok.experimental.Accessors(prefix = "m") +class ConstructorWithToBuilder<T> { + private String mOne, mTwo; + private T foo; + @lombok.Singular private List<T> bars; + @Builder(toBuilder = true) + public ConstructorWithToBuilder(String mOne, @Builder.ObtainVia(field = "foo") T bar) { + } +} +@lombok.experimental.Accessors(prefix = "m") +class StaticWithToBuilder<T, K> { + private String mOne, mTwo; + private T foo; + private K bar; + @lombok.Singular private List<T> bars; + @Builder(toBuilder = true) + public static <Z> StaticWithToBuilder<Z, String> test(String mOne, @Builder.ObtainVia(field = "foo") Z bar) { + return new StaticWithToBuilder<Z, String>(); + } +} diff --git a/website/features/Builder.html b/website/features/Builder.html index 60c69a42..6cf46600 100644 --- a/website/features/Builder.html +++ b/website/features/Builder.html @@ -40,7 +40,7 @@ <li>In the <em>builder</em>: A <code>build()</code> method which calls the static method, passing in each field. It returns the same type that the <em>target</em> returns.</li> <li>In the <em>builder</em>: A sensible <code>toString()</code> implementation.</li> - <li>In the class containing the <em>target</em>: A <code>builder()</code> method, which creates a new instance of the <em>builder</em>.</li> + <li>In the class containing the <em>target</em>: A static <code>builder()</code> method, which creates a new instance of the <em>builder</em>.</li> </ul> Each listed generated element will be silently skipped if that element already exists (disregarding parameter counts and looking only at names). This includes the <em>builder</em> itself: If that class already exists, lombok will simply start injecting fields and methods inside this already existing @@ -59,6 +59,8 @@ <code>@Builder</code> annotation to this all-args-constructor. This only works if you haven't written any explicit constructors yourself. If you do have an explicit constructor, put the <code>@Builder</code> annotation on the constructor instead of on the class. </p><p> + If using <code>@Builder</code> to generate builders to produce instances of your own class (this is always the case unless adding <code>@Builder</code> to a static method that doesn't return your own type), you can use <code>@Builder(toBuilder = true)</code> to also generate an instance method in your class called <code>toBuilder()</code>; it creates a new builder that starts out with all the values of this instance. You can put the <code>@Builder.ObtainVia</code> annotation on the parameters (in case of a constructor or static method) or fields (in case of <code>@Builder</code> on a type) to indicate alternative means by which the value for that field/parameter is obtained from the instance. For example, you can specify a method to be invoked: <code>@Builder.ObtainVia(method = "calculateFoo")</code>. + </p><p> The name of the builder class is <code><em>Foobar</em>Builder</code>, where <em>Foobar</em> is the simplified, title-cased form of the return type of the <em>target</em> - that is, the name of your type for <code>@Builder</code> on constructors and types, and the name of the return type for <code>@Builder</code> on static methods. For example, if <code>@Builder</code> is applied to a class named <code>com.yoyodyne.FancyList<T></code>, then the builder name will be @@ -69,9 +71,10 @@ <li>The <em>builder's class name</em> (default: return type + 'Builder')</li> <li>The <em>build()</em> method's name (default: <code>"build"</code>)</li> <li>The <em>builder()</em> method's name (default: <code>"builder"</code>)</li> + <li>If you want <code>toBuilder()</code> (default: no)</li> </ul> Example usage where all options are changed from their defaults:<br /> - <code>@Builder(builderClassName = "HelloWorldBuilder", buildMethodName = "execute", builderMethodName = "helloWorld")</code><br /> + <code>@Builder(builderClassName = "HelloWorldBuilder", buildMethodName = "execute", builderMethodName = "helloWorld", toBuilder = true)</code><br /> </p> </div> <div class="overview"> @@ -147,6 +150,8 @@ The sorted collections (java.util: <code>SortedSet</code>, <code>NavigableSet</code>, <code>SortedMap</code>, <code>NavigableMap</code> and guava: <code>ImmutableSortedSet</code>, <code>ImmutableSortedMap</code>) require that the type argument of the collection has natural order (implements <code>java.util.Comparable</code>). There is no way to pass an explicit <code>Comparator</code> to use in the builder. </p><p> An <code>ArrayList</code> is used to store added elements as call methods of a <code>@Singular</code> marked field, if the target collection is from the <code>java.util</code> package, <em>even if the collection is a set or map</em>. Because lombok ensures that generated collections are compacted, a new backing instance of a set or map must be constructed anyway, and storing the data as an <code>ArrayList</code> during the build process is more efficient that storing it as a map or set. This behaviour is not externally visible, an an implementation detail of the current implementation of the <code>java.util</code> recipes for <code>@Singular @Builder</code>. + </p><p> + With <code>toBuilder = true</code> applied to static methods, any type parameter on the annotated static method must show up in the returntype. </p> </div> </div> |