Skip to content

Commit

Permalink
[issue projectlombok#2368] [withBy] support for javac
Browse files Browse the repository at this point in the history
  • Loading branch information
rzwitserloot authored and Febell committed Mar 1, 2020
1 parent 7142194 commit 0f7fd11
Show file tree
Hide file tree
Showing 10 changed files with 627 additions and 13 deletions.
9 changes: 9 additions & 0 deletions src/core/lombok/ConfigurationKeys.java
Expand Up @@ -643,6 +643,15 @@ private ConfigurationKeys() {}
*/
public static final ConfigurationKey<FlagUsageType> SUPERBUILDER_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.superBuilder.flagUsage", "Emit a warning or error if @SuperBuilder is used.") {};

// ----- WithBy -----

/**
* lombok configuration: {@code lombok.withBy.flagUsage} = {@code WARNING} | {@code ERROR}.
*
* If set, <em>any</em> usage of {@code @WithBy} results in a warning / error.
*/
public static final ConfigurationKey<FlagUsageType> WITHBY_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.withBy.flagUsage", "Emit a warning or error if @WithBy is used.") {};

// ----- Configuration System -----

/**
Expand Down
32 changes: 31 additions & 1 deletion src/core/lombok/core/handlers/HandlerUtil.java
Expand Up @@ -530,6 +530,20 @@ public static String toWithName(AST<?, ?, ?> ast, AnnotationValues<Accessors> ac
return toAccessorName(ast, accessors, fieldName, isBoolean, "with", "with", false);
}

/**
* Generates a withBy name from a given field name.
*
* Strategy: The same as the {@code toWithName} strategy, but then append {@code "By"} at the end.
*
* @param accessors Accessors configuration.
* @param fieldName the name of the field.
* @param isBoolean if the field is of type 'boolean'. For fields of type {@code java.lang.Boolean}, you should provide {@code false}.
* @return The with name for this field, or {@code null} if this field does not fit expected patterns and therefore cannot be turned into a getter name.
*/
public static String toWithByName(AST<?, ?, ?> ast, AnnotationValues<Accessors> accessors, CharSequence fieldName, boolean isBoolean) {
return toAccessorName(ast, accessors, fieldName, isBoolean, "with", "with", false) + "By";
}

private static String toAccessorName(AST<?, ?, ?> ast, AnnotationValues<Accessors> accessors, CharSequence fieldName, boolean isBoolean,
String booleanPrefix, String normalPrefix, boolean adhereToFluent) {

Expand Down Expand Up @@ -601,6 +615,23 @@ public static List<String> toAllWithNames(AST<?, ?, ?> ast, AnnotationValues<Acc
return toAllAccessorNames(ast, accessors, fieldName, isBoolean, "with", "with", false);
}

/**
* Returns all names of methods that would represent the withBy for a field with the provided name.
*
* For example if {@code isBoolean} is true, then a field named {@code isRunning} would produce:<br />
* {@code [withRunningBy, withIsRunningBy]}
*
* @param accessors Accessors configuration.
* @param fieldName the name of the field.
* @param isBoolean if the field is of type 'boolean'. For fields of type 'java.lang.Boolean', you should provide {@code false}.
*/
public static List<String> toAllWithByNames(AST<?, ?, ?> ast, AnnotationValues<Accessors> accessors, CharSequence fieldName, boolean isBoolean) {
List<String> in = toAllAccessorNames(ast, accessors, fieldName, isBoolean, "with", "with", false);
if (!(in instanceof ArrayList)) in = new ArrayList<String>(in);
for (int i = 0; i < in.size(); i++) in.set(i, in.get(i) + "By");
return in;
}

private static List<String> toAllAccessorNames(AST<?, ?, ?> ast, AnnotationValues<Accessors> accessors, CharSequence fieldName, boolean isBoolean,
String booleanPrefix, String normalPrefix, boolean adhereToFluent) {

Expand Down Expand Up @@ -634,7 +665,6 @@ private static List<String> toAllAccessorNames(AST<?, ?, ?> ast, AnnotationValue
}

return new ArrayList<String>(names);

}

private static List<String> toBaseNames(CharSequence fieldName, boolean isBoolean, boolean fluent) {
Expand Down
12 changes: 6 additions & 6 deletions src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java
Expand Up @@ -1549,7 +1549,7 @@ public enum MemberExistsResult {

/**
* Translates the given field into all possible getter names.
* Convenient wrapper around {@link TransformationsUtil#toAllGetterNames(lombok.core.AnnotationValues, CharSequence, boolean)}.
* Convenient wrapper around {@link HandlerUtil#toAllGetterNames(lombok.core.AnnotationValues, CharSequence, boolean)}.
*/
public static List<String> toAllGetterNames(EclipseNode field, boolean isBoolean) {
return HandlerUtil.toAllGetterNames(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean);
Expand All @@ -1558,15 +1558,15 @@ public static List<String> toAllGetterNames(EclipseNode field, boolean isBoolean
/**
* @return the likely getter name for the stated field. (e.g. private boolean foo; to isFoo).
*
* Convenient wrapper around {@link TransformationsUtil#toGetterName(lombok.core.AnnotationValues, CharSequence, boolean)}.
* Convenient wrapper around {@link HandlerUtil#toGetterName(lombok.core.AnnotationValues, CharSequence, boolean)}.
*/
public static String toGetterName(EclipseNode field, boolean isBoolean) {
return HandlerUtil.toGetterName(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean);
}

/**
* Translates the given field into all possible setter names.
* Convenient wrapper around {@link TransformationsUtil#toAllSetterNames(lombok.core.AnnotationValues, CharSequence, boolean)}.
* Convenient wrapper around {@link HandlerUtil#toAllSetterNames(lombok.core.AnnotationValues, CharSequence, boolean)}.
*/
public static java.util.List<String> toAllSetterNames(EclipseNode field, boolean isBoolean) {
return HandlerUtil.toAllSetterNames(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean);
Expand All @@ -1575,15 +1575,15 @@ public static java.util.List<String> toAllSetterNames(EclipseNode field, boolean
/**
* @return the likely setter name for the stated field. (e.g. private boolean foo; to setFoo).
*
* Convenient wrapper around {@link TransformationsUtil#toSetterName(lombok.core.AnnotationValues, CharSequence, boolean)}.
* Convenient wrapper around {@link HandlerUtil#toSetterName(lombok.core.AnnotationValues, CharSequence, boolean)}.
*/
public static String toSetterName(EclipseNode field, boolean isBoolean) {
return HandlerUtil.toSetterName(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean);
}

/**
* Translates the given field into all possible with names.
* Convenient wrapper around {@link TransformationsUtil#toAllWithNames(lombok.core.AnnotationValues, CharSequence, boolean)}.
* Convenient wrapper around {@link HandlerUtil#toAllWithNames(lombok.core.AnnotationValues, CharSequence, boolean)}.
*/
public static java.util.List<String> toAllWithNames(EclipseNode field, boolean isBoolean) {
return HandlerUtil.toAllWithNames(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean);
Expand All @@ -1592,7 +1592,7 @@ public static java.util.List<String> toAllWithNames(EclipseNode field, boolean i
/**
* @return the likely with name for the stated field. (e.g. private boolean foo; to withFoo).
*
* Convenient wrapper around {@link TransformationsUtil#toWithName(lombok.core.AnnotationValues, CharSequence, boolean)}.
* Convenient wrapper around {@link HandlerUtil#toWithName(lombok.core.AnnotationValues, CharSequence, boolean)}.
*/
public static String toWithName(EclipseNode field, boolean isBoolean) {
return HandlerUtil.toWithName(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean);
Expand Down
112 changes: 112 additions & 0 deletions src/core/lombok/experimental/WithBy.java
@@ -0,0 +1,112 @@
/*
* Copyright (C) 2020 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;

import lombok.AccessLevel;

/**
* Put on any field to make lombok build a 'withBy' - a withFieldNameBy method which produces a clone of this object (except for 1 field which gets a new value).
* <p>
* Complete documentation is found at <a href="https://projectlombok.org/features/experimental/WithBy">the project lombok features page for &#64;WithBy</a>.
* <p>
* Example:
* <pre>
* private &#64;WithBy final int foo;
* private &#64;WithBy final String bar;
* </pre>
*
* will generate:
*
* <pre>
* public SELF_TYPE withFooBy(@lombok.NonNull IntUnaryOperator operator) {
* int foo = operator.apply(this.foo);
* return this.foo == foo ? this : new SELF_TYPE(foo, bar);
* }
* public SELF_TYPE withBarBy(@lombok.NonNull Function&lt;? super String, ? extends String&gt; operator) {
* String bar = operator.apply(this.bar);
* return this.bar == bar ? this : new SELF_TYPE(foo, bar);
* }
* </pre>
* <p>
* This annotation can also be applied to a class, in which case it'll be as if all non-static fields that don't already have
* a {@code WithBy} annotation have the annotation.
* <p>
* This annotation is primarily useful for hierarchical immutable data structures. For example:
*
* <pre>
* class Movie {
* &#64;WithBy private final Director director;
* }
*
* class Director {
* &#64;WithBy private final LocalDate birthDate;
* }
* </pre>
*
* Using plain old {@code @With}, to increment a movie's director's birth date by one, you would write:
*
* <pre>
* movie = movie.withDirector(movie.getDirector().withBirthDate(movie.getDirector().getBirthDate().plusDays(1)));
* </pre>
*
* but with {@code @WithBy}, you'd write:
*
* <pre>
* movie = movie.withDirectorBy(d -> d.withBirthDateBy(bd -> bd.plusDays(1)));
* </pre>
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface WithBy {
/**
* If you want your with method to be non-public, you can specify an alternate access level here.
*
* @return The method will be generated with this access modifier.
*/
AccessLevel value() default AccessLevel.PUBLIC;

/**
* Any annotations listed here are put on the generated method.
* The syntax for this feature depends on JDK version (nothing we can do about that; it's to work around javac bugs).<br>
* up to JDK7:<br>
* {@code @With(onMethod=@__({@AnnotationsGoHere}))}<br>
* from JDK8:<br>
* {@code @With(onMethod_={@AnnotationsGohere})} // note the underscore after {@code onMethod}.
*
* @return List of annotations to apply to the generated method.
*/
AnyAnnotation[] onMethod() default {};

/**
* Placeholder annotation to enable the placement of annotations on the generated code.
* @deprecated Don't use this annotation, ever - Read the documentation.
*/
@Deprecated
@Retention(RetentionPolicy.SOURCE)
@Target({})
@interface AnyAnnotation {}
}

0 comments on commit 0f7fd11

Please sign in to comment.