diff options
28 files changed, 1423 insertions, 113 deletions
diff --git a/doc/changelog.markdown b/doc/changelog.markdown index d1fcb777..1212b77a 100644 --- a/doc/changelog.markdown +++ b/doc/changelog.markdown @@ -2,10 +2,12 @@ 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. * FEATURE: `@NoArgsConstructor(force = true)` can be used to create no args constructors even if final fields are present. * 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/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index 6c595504..67d51895 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -402,14 +402,14 @@ public class ConfigurationKeys { */ public static final ConfigurationKey<FlagUsageType> FIELD_DEFAULTS_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.fieldDefaults.flagUsage", "Emit a warning or error if @FieldDefaults is used.") {}; - // ----- Wither ----- + // ----- Helper ----- /** - * lombok configuration: {@code lombok.wither.flagUsage} = {@code WARNING} | {@code ERROR}. + * lombok configuration: {@code lombok.helper.flagUsage} = {@code WARNING} | {@code ERROR}. * - * If set, <em>any</em> usage of {@code @Wither} results in a warning / error. + * If set, <em>any</em> usage of {@code @Helper} results in a warning / error. */ - public static final ConfigurationKey<FlagUsageType> WITHER_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.wither.flagUsage", "Emit a warning or error if @Wither is used.") {}; + public static final ConfigurationKey<FlagUsageType> HELPER_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.helper.flagUsage", "Emit a warning or error if @Helper is used.") {}; // ----- UtilityClass ----- @@ -418,7 +418,16 @@ public class ConfigurationKeys { * * If set, <em>any</em> usage of {@code @UtilityClass} results in a warning / error. */ - public static final ConfigurationKey<FlagUsageType> UTLITY_CLASS_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.utilityClass.flagUsage", "Emit a warning or error if @UtilityClass is used.") {}; + public static final ConfigurationKey<FlagUsageType> UTILITY_CLASS_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.utilityClass.flagUsage", "Emit a warning or error if @UtilityClass is used.") {}; + + // ----- Wither ----- + + /** + * lombok configuration: {@code lombok.wither.flagUsage} = {@code WARNING} | {@code ERROR}. + * + * If set, <em>any</em> usage of {@code @Wither} results in a warning / error. + */ + public static final ConfigurationKey<FlagUsageType> WITHER_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.wither.flagUsage", "Emit a warning or error if @Wither is used.") {}; // ----- Configuration System ----- 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/EclipseASTVisitor.java b/src/core/lombok/eclipse/EclipseASTVisitor.java index aa19adc6..f5b49cbb 100644 --- a/src/core/lombok/eclipse/EclipseASTVisitor.java +++ b/src/core/lombok/eclipse/EclipseASTVisitor.java @@ -40,7 +40,7 @@ import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; import org.eclipse.jdt.internal.compiler.ast.TypeReference; /** - * Implement so you can ask any JavacAST.Node to traverse depth-first through all children, + * Implement so you can ask any EclipseAST.Node to traverse depth-first through all children, * calling the appropriate visit and endVisit methods. */ public interface EclipseASTVisitor { 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 5acd9249..2ef888fe 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,7 +377,6 @@ 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); } @@ -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) { @@ -499,6 +666,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. @@ -507,53 +684,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/eclipse/handlers/HandleHelper.java b/src/core/lombok/eclipse/handlers/HandleHelper.java new file mode 100644 index 00000000..4e9a7c68 --- /dev/null +++ b/src/core/lombok/eclipse/handlers/HandleHelper.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.eclipse.handlers; + +import static lombok.core.handlers.HandlerUtil.handleExperimentalFlagUsage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.internal.compiler.ASTVisitor; +import org.eclipse.jdt.internal.compiler.ast.ASTNode; +import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration; +import org.eclipse.jdt.internal.compiler.ast.AllocationExpression; +import org.eclipse.jdt.internal.compiler.ast.Annotation; +import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration; +import org.eclipse.jdt.internal.compiler.ast.MessageSend; +import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration; +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.ThisReference; +import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.lookup.BlockScope; +import org.mangosdk.spi.ProviderFor; + +import lombok.ConfigurationKeys; +import lombok.core.AST.Kind; +import lombok.core.AnnotationValues; +import lombok.eclipse.Eclipse; +import lombok.eclipse.EclipseAnnotationHandler; +import lombok.eclipse.EclipseNode; +import lombok.experimental.Helper; + +/** + * Handles the {@code lombok.Cleanup} annotation for eclipse. + */ +@ProviderFor(EclipseAnnotationHandler.class) +public class HandleHelper extends EclipseAnnotationHandler<Helper> { + @Override public void handle(AnnotationValues<Helper> annotation, Annotation ast, EclipseNode annotationNode) { + handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.HELPER_FLAG_USAGE, "@Helper"); + + EclipseNode annotatedType = annotationNode.up(); + EclipseNode containingMethod = annotatedType == null ? null : annotatedType.up(); + if (annotatedType == null || containingMethod == null || annotatedType.getKind() != Kind.TYPE || containingMethod.getKind() != Kind.METHOD) { + annotationNode.addError("@Helper is legal only on method-local classes."); + return; + } + + TypeDeclaration annotatedType_ = (TypeDeclaration) annotatedType.get(); + AbstractMethodDeclaration amd = (AbstractMethodDeclaration) containingMethod.get(); + Statement[] origStatements = amd.statements; + int indexOfType = -1; + for (int i = 0; i < origStatements.length; i++) { + if (origStatements[i] == annotatedType_) { + indexOfType = i; + break; + } + } + + final List<String> knownMethodNames = new ArrayList<String>(); + + for (AbstractMethodDeclaration methodOfHelper : annotatedType_.methods) { + if (!(methodOfHelper instanceof MethodDeclaration)) continue; + char[] name = methodOfHelper.selector; + if (name != null && name.length > 0 && name[0] != '<') knownMethodNames.add(new String(name)); + } + + Collections.sort(knownMethodNames); + final String[] knownMethodNames_ = knownMethodNames.toArray(new String[knownMethodNames.size()]); + + final char[] helperName = new char[annotatedType_.name.length + 1]; + final boolean[] helperUsed = new boolean[1]; + helperName[0] = '$'; + System.arraycopy(annotatedType_.name, 0, helperName, 1, helperName.length - 1); + + ASTVisitor visitor = new ASTVisitor() { + @Override public boolean visit(MessageSend messageSend, BlockScope scope) { + if (messageSend.receiver instanceof ThisReference) { + if ((((ThisReference) messageSend.receiver).bits & ASTNode.IsImplicitThis) == 0) return true; + } else if (messageSend.receiver != null) return true; + + char[] name = messageSend.selector; + if (name == null || name.length == 0 || name[0] == '<') return true; + String n = new String(name); + if (Arrays.binarySearch(knownMethodNames_, n) < 0) return true; + messageSend.receiver = new SingleNameReference(helperName, Eclipse.pos(messageSend)); + helperUsed[0] = true; + return true; + } + }; + + for (int i = indexOfType + 1; i < origStatements.length; i++) { + origStatements[i].traverse(visitor, null); + } + + if (!helperUsed[0]) { + annotationNode.addWarning("No methods of this helper class are ever used."); + return; + } + + Statement[] newStatements = new Statement[origStatements.length + 1]; + System.arraycopy(origStatements, 0, newStatements, 0, indexOfType + 1); + System.arraycopy(origStatements, indexOfType + 1, newStatements, indexOfType + 2, origStatements.length - indexOfType - 1); + LocalDeclaration decl = new LocalDeclaration(helperName, 0, 0); + decl.modifiers |= ClassFileConstants.AccFinal; + AllocationExpression alloc = new AllocationExpression(); + alloc.type = new SingleTypeReference(annotatedType_.name, 0L); + decl.initialization = alloc; + decl.type = new SingleTypeReference(annotatedType_.name, 0L); + SetGeneratedByVisitor sgbvVisitor = new SetGeneratedByVisitor(annotationNode.get()); + decl.traverse(sgbvVisitor, null); + newStatements[indexOfType + 1] = decl; + amd.statements = newStatements; + } +} diff --git a/src/core/lombok/eclipse/handlers/HandleUtilityClass.java b/src/core/lombok/eclipse/handlers/HandleUtilityClass.java index c3c85ad4..199ce102 100644 --- a/src/core/lombok/eclipse/handlers/HandleUtilityClass.java +++ b/src/core/lombok/eclipse/handlers/HandleUtilityClass.java @@ -60,7 +60,7 @@ import lombok.experimental.UtilityClass; @ProviderFor(EclipseAnnotationHandler.class) public class HandleUtilityClass extends EclipseAnnotationHandler<UtilityClass> { @Override public void handle(AnnotationValues<UtilityClass> annotation, Annotation ast, EclipseNode annotationNode) { - handleFlagUsage(annotationNode, ConfigurationKeys.UTLITY_CLASS_FLAG_USAGE, "@UtilityClass"); + handleFlagUsage(annotationNode, ConfigurationKeys.UTILITY_CLASS_FLAG_USAGE, "@UtilityClass"); EclipseNode typeNode = annotationNode.up(); if (!checkLegality(typeNode, annotationNode)) return; diff --git a/src/core/lombok/experimental/Helper.java b/src/core/lombok/experimental/Helper.java new file mode 100644 index 00000000..34745cbe --- /dev/null +++ b/src/core/lombok/experimental/Helper.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.experimental; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use on a method local class to indicate that all methods inside should be exposed to the rest of + * the method as if they were helper methods. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface Helper {} diff --git a/src/core/lombok/javac/handlers/HandleBuilder.java b/src/core/lombok/javac/handlers/HandleBuilder.java index 066e371e..f0139b47 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/src/core/lombok/javac/handlers/HandleHelper.java b/src/core/lombok/javac/handlers/HandleHelper.java new file mode 100644 index 00000000..99131f70 --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleHelper.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import static lombok.core.handlers.HandlerUtil.*; +import static lombok.javac.handlers.JavacHandlerUtil.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; + +import org.mangosdk.spi.ProviderFor; + +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.TreeVisitor; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.tree.JCTree.JCAnnotation; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.JCTree.JCExpression; +import com.sun.tools.javac.tree.JCTree.JCIdent; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; +import com.sun.tools.javac.tree.JCTree.JCStatement; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.ListBuffer; +import com.sun.tools.javac.util.Name; + +import lombok.ConfigurationKeys; +import lombok.core.AST.Kind; +import lombok.core.AnnotationValues; +import lombok.experimental.Helper; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; +import lombok.javac.JavacTreeMaker; + +@ProviderFor(JavacAnnotationHandler.class) +public class HandleHelper extends JavacAnnotationHandler<Helper> { + @Override public void handle(AnnotationValues<Helper> annotation, JCAnnotation ast, JavacNode annotationNode) { + handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.HELPER_FLAG_USAGE, "@Helper"); + + deleteAnnotationIfNeccessary(annotationNode, Helper.class); + JavacNode annotatedType = annotationNode.up(); + JavacNode containingMethod = annotatedType == null ? null : annotatedType.up(); + + if (annotatedType == null || containingMethod == null || annotatedType.getKind() != Kind.TYPE || containingMethod.getKind() != Kind.METHOD) { + annotationNode.addError("@Helper is legal only on method-local classes."); + return; + } + + JCClassDecl annotatedType_ = (JCClassDecl) annotatedType.get(); + JCMethodDecl amd = (JCMethodDecl) containingMethod.get(); + List<JCStatement> origStatements = amd.body.stats; + Iterator<JCStatement> it = origStatements.iterator(); + while (it.hasNext()) { + if (it.next() == annotatedType_) { + break; + } + } + + java.util.List<String> knownMethodNames = new ArrayList<String>(); + + for (JavacNode ch : annotatedType.down()) { + if (ch.getKind() != Kind.METHOD) continue; + String n = ch.getName(); + if (n == null || n.isEmpty() || n.charAt(0) == '<') continue; + knownMethodNames.add(n); + } + + Collections.sort(knownMethodNames); + final String[] knownMethodNames_ = knownMethodNames.toArray(new String[knownMethodNames.size()]); + + final Name helperName = annotationNode.toName("$" + annotatedType_.name); + final boolean[] helperUsed = new boolean[1]; + final JavacTreeMaker maker = annotationNode.getTreeMaker(); + + TreeVisitor<Void, Void> visitor = new TreeScanner<Void, Void>() { + @Override public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + JCMethodInvocation jcmi = (JCMethodInvocation) node; + apply(jcmi); + return super.visitMethodInvocation(node, p); + } + + private void apply(JCMethodInvocation jcmi) { + if (!(jcmi.meth instanceof JCIdent)) return; + JCIdent jci = (JCIdent) jcmi.meth; + if (Arrays.binarySearch(knownMethodNames_, jci.name.toString()) < 0) return; + jcmi.meth = maker.Select(maker.Ident(helperName), jci.name); + helperUsed[0] = true; + } + }; + + while (it.hasNext()) { + JCStatement stat = it.next(); + stat.accept(visitor, null); + } + + if (!helperUsed[0]) { + annotationNode.addWarning("No methods of this helper class are ever used."); + return; + } + + ListBuffer<JCStatement> newStatements = new ListBuffer<JCStatement>(); + + boolean mark = false; + for (JCStatement stat : origStatements) { + newStatements.append(stat); + if (mark || stat != annotatedType_) continue; + mark = true; + JCExpression init = maker.NewClass(null, List.<JCExpression>nil(), maker.Ident(annotatedType_.name), List.<JCExpression>nil(), null); + JCExpression varType = maker.Ident(annotatedType_.name); + JCVariableDecl decl = maker.VarDef(maker.Modifiers(Flags.FINAL), helperName, varType, init); + newStatements.append(decl); + } + amd.body.stats = newStatements.toList(); + } +} diff --git a/src/core/lombok/javac/handlers/HandleUtilityClass.java b/src/core/lombok/javac/handlers/HandleUtilityClass.java index a4f8cb45..010c05a5 100644 --- a/src/core/lombok/javac/handlers/HandleUtilityClass.java +++ b/src/core/lombok/javac/handlers/HandleUtilityClass.java @@ -52,7 +52,7 @@ import com.sun.tools.javac.util.Name; @ProviderFor(JavacAnnotationHandler.class) public class HandleUtilityClass extends JavacAnnotationHandler<UtilityClass> { @Override public void handle(AnnotationValues<UtilityClass> annotation, JCAnnotation ast, JavacNode annotationNode) { - handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.UTLITY_CLASS_FLAG_USAGE, "@UtilityClass"); + handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.UTILITY_CLASS_FLAG_USAGE, "@UtilityClass"); deleteAnnotationIfNeccessary(annotationNode, UtilityClass.class); 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-delombok/Helper.java b/test/transform/resource/after-delombok/Helper.java new file mode 100644 index 00000000..52f50dd2 --- /dev/null +++ b/test/transform/resource/after-delombok/Helper.java @@ -0,0 +1,16 @@ +class HelperTest { + void test() { + class H1 { + void foo() { + System.out.println("Hello"); + } + } + final H1 $H1 = new H1(); + $H1.foo(); + class H2 { + void bar() { + $H1.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/after-ecj/Helper.java b/test/transform/resource/after-ecj/Helper.java new file mode 100644 index 00000000..1b4e8bd2 --- /dev/null +++ b/test/transform/resource/after-ecj/Helper.java @@ -0,0 +1,26 @@ +import lombok.experimental.Helper; +class HelperTest { + HelperTest() { + super(); + } + void test() { + @Helper class H1 { + H1() { + super(); + } + void foo() { + System.out.println("Hello"); + } + } + final H1 $H1 = new H1(); + $H1.foo(); + @Helper class H2 { + H2() { + super(); + } + void bar() { + $H1.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/test/transform/resource/before/Helper.java b/test/transform/resource/before/Helper.java new file mode 100644 index 00000000..145d740f --- /dev/null +++ b/test/transform/resource/before/Helper.java @@ -0,0 +1,19 @@ +import lombok.experimental.Helper; + +class HelperTest { + void test() { + @Helper class H1 { + void foo() { + System.out.println("Hello"); + } + } + + foo(); + + @Helper class H2 { + void bar() { + foo(); + } + } + } +}
\ No newline at end of file diff --git a/test/transform/resource/messages-delombok/Helper.java.messages b/test/transform/resource/messages-delombok/Helper.java.messages new file mode 100644 index 00000000..05260c0b --- /dev/null +++ b/test/transform/resource/messages-delombok/Helper.java.messages @@ -0,0 +1 @@ +13 No methods of this helper class are ever used.
\ No newline at end of file diff --git a/test/transform/resource/messages-ecj/Helper.java.messages b/test/transform/resource/messages-ecj/Helper.java.messages new file mode 100644 index 00000000..7207e136 --- /dev/null +++ b/test/transform/resource/messages-ecj/Helper.java.messages @@ -0,0 +1 @@ +13 No methods of this helper class are ever used. diff --git a/usage_examples/experimental/Helper_post.jpage b/usage_examples/experimental/Helper_post.jpage new file mode 100644 index 00000000..04a97b9f --- /dev/null +++ b/usage_examples/experimental/Helper_post.jpage @@ -0,0 +1,14 @@ +public class HelperExample { + int someMethod(int arg1) { + int localVar = 5; + + class Helpers { + int helperMethod(int arg) { + return arg + localVar; + } + } + Helpers $Helpers = new Helpers(); + + return $Helpers.helperMethod(10); + } +} diff --git a/usage_examples/experimental/Helper_pre.jpage b/usage_examples/experimental/Helper_pre.jpage new file mode 100644 index 00000000..cd86ef3c --- /dev/null +++ b/usage_examples/experimental/Helper_pre.jpage @@ -0,0 +1,15 @@ +import lombok.experimental.Helper; + +public class HelperExample { + int someMethod(int arg1) { + int localVar = 5; + + @Helper class Helpers { + int helperMethod(int arg) { + return arg + localVar; + } + } + + return helperMethod(10); + } +} 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> diff --git a/website/features/experimental/Helper.html b/website/features/experimental/Helper.html new file mode 100644 index 00000000..7658b780 --- /dev/null +++ b/website/features/experimental/Helper.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<html><head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <link rel="stylesheet" type="text/css" href="../../logi/reset.css" /> + <link rel="stylesheet" type="text/css" href="../features.css" /> + <link rel="shortcut icon" href="../../favicon.ico" type="image/x-icon" /> + <meta name="description" content="Spice up your java" /> + <title>@Helper</title> +</head><body><div id="pepper"> + <div class="minimumHeight"></div> + <div class="meat"> + <div class="header"><a href="../../index.html">Project Lombok</a></div> + <h1>@Helper</h1> + <div class="byline">With a little help from my friends... Helper methods for java.</div> + <div class="since"> + <h3>Since</h3> + <p> + <code>@Helper</code> was introduced as an experimental feature in lombok v1.16.6. + </p> + </div> + <div class="experimental"> + <h3>Experimental</h3> + <p> + Experimental because: + <ul> + <li>Lambdas with general function types offer an alternative strategy.</li> + <li>Perhaps a way to make helper methods with less boilerplate is possible, making this feature obsolete.</li> + </ul> + Current status: <em>unknown</em> - We don't have enough experience with this feature to make predictions on its future. + </div> + <div class="overview"> + <h3>Overview</h3> + <p> + This annotation lets you put methods in methods. You might not know this, but you can declare classes inside methods, and the methods in this class can access any (effectively) final local variable or parameter defined and set before the declaration. Unfortunately, to actually call any methods you'd have to make an instance of this method local class first, but that's where <code>@Helper</code> comes in and helps you out! Annotate a method local class with <code>@Helper</code> and it's as if all the methods in that helper class are methods that you can call directly, just as if java had allowed methods to exist inside methods. + </p><p> + Normally you'd have to declare an instance of your helper, for example: <code>HelperClass h = new HelperClass();</code> directly after declaring your helper class, and then call methods in your helper class with <code>h.helperMethod();</code>. With <code>@Helper</code>, both of these things are no longer needed: You do not need to waste a line of code declaring an instance of the helper, and you don't need to prefix all your calls to helper methods with <code>nameOfHelperInstance.</code> + </p> + </div> + <div class="snippets"> + <div class="pre"> + <h3>With Lombok</h3> + <div class="snippet">@HTML_PRE@</div> + </div> + <div class="sep"></div> + <div class="post"> + <h3>Vanilla Java</h3> + <div class="snippet">@HTML_POST@</div> + </div> + </div> + <div style="clear: left;"></div> + <div class="overview"> + <h3>Small print</h3><div class="smallprint"> + <p> + <code>@Helper</code> requires that the helper class has a no-args constructor. A compiler error will be generated if this is not the case. + </p><p> + Currently, the instance of your helper that's made under the hood is called <code>$Foo</code>, where <code>Foo</code> is the name of your helper. We might change this in the future; please don't rely on this variable existing. We might even replace this later with a sibling method instead. + </p><p> + Please don't rely on <code>this</code> making any sense in the helper method code. You can refer to the real 'this' by using the syntax <code>NameOfMyClass.this</code>. + </p><p> + <em>ANY</em> unqualified method call in code that exists <em>below</em> the declaration of the helper method with the same name as any method in the helper is assumed to be a call to the helper. If the arguments don't end up being compatible, you get a compiler error. + </p><p> + Unless you're using JDK8 or higher (which introduced the concept of 'effectively final'), you'll have to declare local variables and parameters as <code>final</code> if you wish to refer to them in your method local class. This is a java limitation, not something specific to lombok's <code>@Helper</code>. + </p> + </div> + </div> + <div class="footer"> + <a href="index.html">Back to experimental features</a> | <a href="UtilityClass.html">Previous feature (@UtilityClass)</a> | <span class="disabled">Next feature</span><br /> + <a href="../../credits.html" class="creditsLink">credits</a> | <span class="copyright">Copyright © 2009-2015 The Project Lombok Authors, licensed under the <a href="http://www.opensource.org/licenses/mit-license.php">MIT license</a>.</span> + </div> + <div style="clear: both;"></div> + </div> +</div> +<script type="text/javascript"> + var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); + document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); +</script> +<script type="text/javascript"> + try { + var pageTracker = _gat._getTracker("UA-9884254-1"); + pageTracker._trackPageview(); + } catch(err) {} +</script> +</body></html> diff --git a/website/features/experimental/UtilityClass.html b/website/features/experimental/UtilityClass.html index 5526ec77..3c79b35e 100644 --- a/website/features/experimental/UtilityClass.html +++ b/website/features/experimental/UtilityClass.html @@ -61,7 +61,7 @@ </div> </div> <div class="footer"> - <a href="index.html">Back to experimental features</a> | <a href="onX.html">Previous feature (onX)</a> | <span class="disabled">Next feature</span><br /> + <a href="index.html">Back to experimental features</a> | <a href="onX.html">Previous feature (onX)</a> | <a href="Helper.html">Next feature (@Helper)</a><br /> <a href="../../credits.html" class="creditsLink">credits</a> | <span class="copyright">Copyright © 2009-2015 The Project Lombok Authors, licensed under the <a href="http://www.opensource.org/licenses/mit-license.php">MIT license</a>.</span> </div> <div style="clear: both;"></div> diff --git a/website/features/experimental/index.html b/website/features/experimental/index.html index 3f2d2802..ce21f819 100644 --- a/website/features/experimental/index.html +++ b/website/features/experimental/index.html @@ -36,6 +36,8 @@ <dd>Sup dawg, we heard you like annotations, so we put annotations in your annotations so you can annotate while you're annotating.</dd> <dt><a href="UtilityClass.html"><code>@UtilityClass</code></a></dt> <dd>Utility, metility, wetility! Utility classes for the masses.</dd> + <dt><a href="Helper.html"><code>@Helper</code></a></dt> + <dd>With a little help from my friends... Helper methods for java.</dd> </dl> </div> <div class="overview confKeys"> |