Skip to content
This repository was archived by the owner on Oct 16, 2024. It is now read-only.

Commit 23d6cf8

Browse files
authored
#438 Change name of build and other methods
If generating public build, buildPartial, clear or mergeFrom methods would cause a compiler error, instead make them package-protected and, if necessary, find an alternative method name by prepending an underscore and appending Impl and optionally a number.
2 parents 5baf61a + 1eedb99 commit 23d6cf8

File tree

14 files changed

+1646
-14
lines changed

14 files changed

+1646
-14
lines changed

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,27 @@ For instance, `assertEquals` in JUnit relies on equality; it will not know to ch
533533
If you are only testing a subset of your fields for equality, consider separating your class in two, as you may have accidentally combined the key and the value of a map into a single object, and you may find your code becomes healthier after the separation.
534534
Alternatively, creating a custom [Comparator] will make it explicit that you are not using the natural definition of equality.
535535

536+
### Custom conventional method names
537+
538+
If for any reason your types cannot use the conventional method names (`build`, `buildPartial`, `clear` and `mergeFrom`), you can force FreeBuilder to generate package protected implementations, and even select alternative fallback names if necessary, by declaring an alternative visibility and/or incompatible signature. If the default name is not available, FreeBuilder will prepend an underscore and append "Impl" (and, if necessary, a number), e.g. `build` becomes `_buildImpl`.
539+
540+
```java
541+
public interface MyType {
542+
class Builder extends MyType_Builder {
543+
public OtherDataType build() {
544+
// This signature is not compatible with the default build method.
545+
// FreeBuilder will instead declare a package-scoped _buildImpl.
546+
...
547+
}
548+
public DataType buildMyType() {
549+
return _buildImpl();
550+
}
551+
}
552+
}
553+
```
554+
555+
Note that this will, unfortunately, disable FreeBuilder's [enhanced support for nested builders](#nested-buildable-types) for this type, as it needs to be able to call these methods.
556+
536557
### Custom functional interfaces
537558

538559
FreeBuilder's generated map and mutate methods take [UnaryOperator] or [Consumer] functional interfaces. If you need to use a different functional interface, you can override the generated methods in your Builder and change the parameter type. FreeBuilder will spot the incompatible override and change the code it generates to match:

generated/main/java/org/inferred/freebuilder/processor/Datatype_Builder.java

+386
Large diffs are not rendered by default.

src/main/java/org/inferred/freebuilder/processor/Analyser.java

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static com.google.common.collect.Iterables.transform;
2222

2323
import static org.inferred.freebuilder.processor.GwtSupport.gwtMetadata;
24+
import static org.inferred.freebuilder.processor.NamePicker.pickName;
2425
import static org.inferred.freebuilder.processor.model.MethodFinder.methodsOn;
2526
import static org.inferred.freebuilder.processor.model.ModelUtils.asElement;
2627
import static org.inferred.freebuilder.processor.model.ModelUtils.getReturnType;
@@ -144,6 +145,13 @@ GeneratedType analyse(TypeElement type) throws CannotGenerateCodeException {
144145
.setPartialType(partialType.withParameters(typeParameters))
145146
.setPropertyEnum(propertyType.withParameters())
146147
.putAllStandardMethodUnderrides(findUnderriddenMethods(methods))
148+
.setBuildMethod(pickName(builder, elements, types, type.asType(), "build"))
149+
.setBuildPartialMethod(pickName(builder, elements, types, type.asType(), "buildPartial"))
150+
.setClearMethod(pickName(builder, elements, types, builder, "clear"))
151+
.setMergeFromBuilderMethod(pickName(
152+
builder, elements, types, builder, "mergeFrom", builder))
153+
.setMergeFromValueMethod(pickName(
154+
builder, elements, types, builder, "mergeFrom", type.asType()))
147155
.setHasToBuilderMethod(hasToBuilderMethod(
148156
builder, constructionAndExtension.isExtensible(), methods))
149157
.setBuilderSerializable(shouldBuilderBeSerializable(builder))

src/main/java/org/inferred/freebuilder/processor/BuildableType.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.inferred.freebuilder.processor;
1717

18+
import static org.inferred.freebuilder.processor.NamePicker.pickName;
1819
import static org.inferred.freebuilder.processor.model.ModelUtils.asElement;
1920
import static org.inferred.freebuilder.processor.model.ModelUtils.findAnnotationMirror;
2021
import static org.inferred.freebuilder.processor.model.ModelUtils.needsSafeVarargs;
@@ -24,6 +25,7 @@
2425
import static javax.lang.model.element.Modifier.PUBLIC;
2526

2627
import org.inferred.freebuilder.FreeBuilder;
28+
import org.inferred.freebuilder.processor.Datatype.Visibility;
2729
import org.inferred.freebuilder.processor.source.Excerpt;
2830
import org.inferred.freebuilder.processor.source.Excerpts;
2931
import org.inferred.freebuilder.processor.source.Type;
@@ -134,7 +136,17 @@ public static Optional<DeclaredType> maybeBuilder(
134136
* probably skip this for @FreeBuilder-types anyway, to avoid extra types whenever possible,
135137
* which leaves a lot of complicated code supporting a currently non-existent edge case.
136138
*/
137-
if (!findAnnotationMirror(element, FreeBuilder.class).isPresent()) {
139+
if (findAnnotationMirror(element, FreeBuilder.class).isPresent()) {
140+
// Make sure the user isn't preventing us generating required methods.
141+
if (methodIsObscured(builderMirror, elements, types, type, "build")
142+
|| methodIsObscured(builderMirror, elements, types, type, "buildPartial")
143+
|| methodIsObscured(builderMirror, elements, types, builderMirror, "clear")
144+
|| methodIsObscured(
145+
builderMirror, elements, types, builderMirror, "mergeFrom", builderMirror)
146+
|| methodIsObscured(builderMirror, elements, types, builderMirror, "mergeFrom", type)) {
147+
return Optional.empty();
148+
}
149+
} else {
138150
List<ExecutableElement> methods = elements.getAllMembers(builder)
139151
.stream()
140152
.flatMap(METHODS)
@@ -165,6 +177,18 @@ public static Optional<DeclaredType> maybeBuilder(
165177
return Optional.of(builderMirror);
166178
}
167179

180+
private static boolean methodIsObscured(
181+
DeclaredType targetType,
182+
Elements elements,
183+
Types types,
184+
DeclaredType returnType,
185+
String methodName,
186+
DeclaredType... parameterTypes) {
187+
NameAndVisibility buildMethod =
188+
pickName(targetType, elements, types, returnType, methodName, parameterTypes);
189+
return buildMethod.name() != methodName || buildMethod.visibility() != Visibility.PUBLIC;
190+
}
191+
168192
public static BuildableType create(
169193
DeclaredType datatype,
170194
DeclaredType builder,

src/main/java/org/inferred/freebuilder/processor/Datatype.java

+20
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,21 @@ public UnderrideLevel standardMethodUnderride(StandardMethod standardMethod) {
128128
/** Returns whether the value type has a toBuilder method that needs to be generated. */
129129
public abstract boolean getHasToBuilderMethod();
130130

131+
/** Returns the build method to be generated. */
132+
public abstract NameAndVisibility getBuildMethod();
133+
134+
/** Returns the partial build method to be generated. */
135+
public abstract NameAndVisibility getBuildPartialMethod();
136+
137+
/** Returns the clear method to be generated. */
138+
public abstract NameAndVisibility getClearMethod();
139+
140+
/** Returns the mergeFrom(Builder) method to be generated. */
141+
public abstract NameAndVisibility getMergeFromBuilderMethod();
142+
143+
/** Returns the mergeFrom(Value) method to be generated. */
144+
public abstract NameAndVisibility getMergeFromValueMethod();
145+
131146
/** Returns a list of annotations that should be applied to the generated builder class. */
132147
public abstract ImmutableList<Excerpt> getGeneratedBuilderAnnotations();
133148

@@ -148,6 +163,11 @@ public Builder toBuilder() {
148163
public static class Builder extends Datatype_Builder {
149164

150165
public Builder() {
166+
super.setBuildMethod(NameAndVisibility.of("build", Visibility.PUBLIC));
167+
super.setBuildPartialMethod(NameAndVisibility.of("buildPartial", Visibility.PUBLIC));
168+
super.setClearMethod(NameAndVisibility.of("clear", Visibility.PUBLIC));
169+
super.setMergeFromBuilderMethod(NameAndVisibility.of("mergeFrom", Visibility.PUBLIC));
170+
super.setMergeFromValueMethod(NameAndVisibility.of("mergeFrom", Visibility.PUBLIC));
151171
super.setValueTypeVisibility(Visibility.PRIVATE);
152172
super.setHasToBuilderMethod(false);
153173
}

src/main/java/org/inferred/freebuilder/processor/GeneratedBuilder.java

+30-10
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,9 @@ private void addStaticFromMethod(SourceBuilder code) {
149149
code.addLine(" if (value instanceof %s) {", rebuildable.getQualifiedName())
150150
.addLine(" return ((%s) value).toBuilder();", rebuildable)
151151
.addLine(" } else {")
152-
.addLine(" return %s.mergeFrom(value);",
153-
builderFactory.newBuilder(datatype.getBuilder(), EXPLICIT_TYPES))
152+
.addLine(" return %s.%s(value);",
153+
builderFactory.newBuilder(datatype.getBuilder(), EXPLICIT_TYPES),
154+
datatype.getMergeFromValueMethod().name())
154155
.addLine(" }");
155156
}
156157
code.addLine("}");
@@ -182,7 +183,10 @@ private void addBuildMethod(SourceBuilder code) {
182183
.addLine(" * @throws IllegalStateException if any field has not been set");
183184
}
184185
code.addLine(" */")
185-
.addLine("public %s build() {", datatype.getType());
186+
.addLine("%s%s %s() {",
187+
datatype.getBuildMethod().visibility(),
188+
datatype.getType(),
189+
datatype.getBuildMethod().name());
186190
if (hasRequiredProperties) {
187191
code.add(PreconditionExcerpts.checkState(
188192
"%1$s.isEmpty()", "Not set: %1$s", UNSET_PROPERTIES));
@@ -199,7 +203,11 @@ private void addMergeFromValueMethod(SourceBuilder code) {
199203
.addLine(" *")
200204
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName())
201205
.addLine(" */")
202-
.addLine("public %s mergeFrom(%s value) {", datatype.getBuilder(), datatype.getType());
206+
.addLine("%s%s %s(%s value) {",
207+
datatype.getMergeFromValueMethod().visibility(),
208+
datatype.getBuilder(),
209+
datatype.getMergeFromValueMethod().name(),
210+
datatype.getType());
203211
generatorsByProperty.values().forEach(generator -> generator.addMergeFromValue(code, "value"));
204212
code.addLine(" return (%s) this;", datatype.getBuilder())
205213
.addLine("}");
@@ -213,7 +221,10 @@ private void addMergeFromBuilderMethod(SourceBuilder code) {
213221
.addLine(" *")
214222
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName())
215223
.addLine(" */")
216-
.addLine("public %1$s mergeFrom(%1$s template) {", datatype.getBuilder());
224+
.addLine("%1$s%2$s %3$s(%2$s template) {",
225+
datatype.getMergeFromBuilderMethod().visibility(),
226+
datatype.getBuilder(),
227+
datatype.getMergeFromBuilderMethod().name());
217228
generatorsByProperty.values().forEach(generator -> {
218229
generator.addMergeFromBuilder(code, "template");
219230
});
@@ -235,7 +246,10 @@ private void addClearMethod(SourceBuilder code) {
235246
.addLine(" *")
236247
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName())
237248
.addLine(" */")
238-
.addLine("public %s clear() {", datatype.getBuilder());
249+
.addLine("%s%s %s() {",
250+
datatype.getClearMethod().visibility(),
251+
datatype.getBuilder(),
252+
datatype.getClearMethod().name());
239253
generatorsByProperty.values().forEach(codeGenerator -> {
240254
codeGenerator.addClearField(code);
241255
});
@@ -268,7 +282,9 @@ private void addBuildPartialMethod(SourceBuilder code) {
268282
}
269283
code.addLine("will propagate the partial status of its input, overriding")
270284
.addLine(" * %s to return another partial.",
271-
datatype.getBuilder().javadocNoArgMethodLink("build").withText("build()"))
285+
datatype.getBuilder()
286+
.javadocNoArgMethodLink(datatype.getBuildMethod().name())
287+
.withText(datatype.getBuildMethod().name() + "()"))
272288
.addLine(" * This allows for robust tests of modify-rebuild code.")
273289
.addLine(" *")
274290
.addLine(" * <p>Partials should only ever be used in tests. They permit writing robust")
@@ -279,7 +295,10 @@ private void addBuildPartialMethod(SourceBuilder code) {
279295
if (code.feature(GUAVA).isAvailable()) {
280296
code.addLine("@%s()", VisibleForTesting.class);
281297
}
282-
code.addLine("public %s buildPartial() {", datatype.getType())
298+
code.addLine("%s%s %s() {",
299+
datatype.getBuildPartialMethod().visibility(),
300+
datatype.getType(),
301+
datatype.getBuildPartialMethod().name())
283302
.addLine(" return %s(this);", datatype.getPartialType().constructor())
284303
.addLine("}");
285304
}
@@ -535,8 +554,9 @@ private void addPartialToBuilderMethod(SourceBuilder code) {
535554
code.addLine("")
536555
.addLine(" private static class PartialBuilder%s extends %s {",
537556
datatype.getType().declarationParameters(), datatype.getBuilder())
538-
.addLine(" @Override public %s build() {", datatype.getType())
539-
.addLine(" return buildPartial();")
557+
.addLine(" @Override public %s %s() {",
558+
datatype.getType(), datatype.getBuildMethod().name())
559+
.addLine(" return %s();", datatype.getBuildPartialMethod().name())
540560
.addLine(" }")
541561
.addLine(" }");
542562
}

src/main/java/org/inferred/freebuilder/processor/GwtSupport.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ private void addInstantiateInstance(SourceBuilder code) {
141141
.addLine(" }");
142142
}
143143
}
144-
code.addLine(" return (%s) %s.build();", datatype.getValueType(), builder)
144+
code.addLine(" return (%s) %s.%s();",
145+
datatype.getValueType(), builder, datatype.getBuildMethod().name())
145146
.addLine(" }");
146147
}
147148

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.inferred.freebuilder.processor;
2+
3+
import org.inferred.freebuilder.processor.Datatype.Visibility;
4+
5+
import java.util.Objects;
6+
7+
public class NameAndVisibility {
8+
9+
public static NameAndVisibility of(String name, Visibility visibility) {
10+
return new NameAndVisibility(name, visibility);
11+
}
12+
13+
private final String name;
14+
private final Visibility visibility;
15+
16+
private NameAndVisibility(String name, Visibility visibility) {
17+
this.name = name;
18+
this.visibility = visibility;
19+
}
20+
21+
public String name() {
22+
return name;
23+
}
24+
25+
public Visibility visibility() {
26+
return visibility;
27+
}
28+
29+
@Override
30+
public int hashCode() {
31+
return Objects.hash(name, visibility);
32+
}
33+
34+
@Override
35+
public boolean equals(Object obj) {
36+
if (this == obj) {
37+
return true;
38+
}
39+
if (!(obj instanceof NameAndVisibility)) {
40+
return false;
41+
}
42+
NameAndVisibility other = (NameAndVisibility) obj;
43+
return Objects.equals(name, other.name) && visibility == other.visibility;
44+
}
45+
46+
@Override
47+
public String toString() {
48+
return "NameAndVisibility{name=" + name + ", visibility=" + visibility + "}";
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package org.inferred.freebuilder.processor;
2+
3+
import static org.inferred.freebuilder.processor.model.MethodFinder.methodsOn;
4+
import static org.inferred.freebuilder.processor.model.ModelUtils.asElement;
5+
import static org.inferred.freebuilder.processor.model.ModelUtils.getReturnType;
6+
7+
import static java.util.stream.Collectors.toMap;
8+
9+
import org.inferred.freebuilder.processor.Datatype.Visibility;
10+
11+
import java.util.Map;
12+
import java.util.function.Predicate;
13+
import java.util.stream.Collector;
14+
15+
import javax.lang.model.element.ExecutableElement;
16+
import javax.lang.model.element.Modifier;
17+
import javax.lang.model.element.VariableElement;
18+
import javax.lang.model.type.DeclaredType;
19+
import javax.lang.model.type.TypeMirror;
20+
import javax.lang.model.util.Elements;
21+
import javax.lang.model.util.Types;
22+
23+
public class NamePicker {
24+
25+
/** Find an available name and visibility for a method. */
26+
public static NameAndVisibility pickName(
27+
DeclaredType targetType,
28+
Elements elements,
29+
Types types,
30+
TypeMirror returnType,
31+
String preferredName,
32+
TypeMirror... parameterTypes) {
33+
Map<String, ExecutableElement> methodsByName =
34+
methodsOn(asElement(targetType), elements, errorType -> { })
35+
.stream()
36+
.filter(matchingErasedParameters(types, parameterTypes))
37+
.collect(byName());
38+
39+
String name = preferredName;
40+
Visibility visibility = Visibility.PUBLIC;
41+
42+
for (int attempt = 1; true; attempt++) {
43+
ExecutableElement method = methodsByName.get(name);
44+
if (method == null) {
45+
// No (parameterless) method exists with this name, so we can create one
46+
return NameAndVisibility.of(name, visibility);
47+
}
48+
boolean sufficientVisibility = !method.getModifiers().contains(Modifier.PRIVATE);
49+
boolean correctSignature = parametersMatchExactly(types, method, parameterTypes);
50+
TypeMirror actualReturnType = getReturnType(targetType, method, types);
51+
boolean correctReturnType = types.isSameType(actualReturnType, returnType);
52+
if (sufficientVisibility && correctSignature && correctReturnType) {
53+
// A method exists with this name, but it is not incompatible
54+
if (!method.getModifiers().contains(Modifier.PUBLIC)) {
55+
visibility = Visibility.PACKAGE;
56+
}
57+
return NameAndVisibility.of(name, visibility);
58+
}
59+
60+
// This method name is already taken by an incompatible method; try a different name.
61+
name = "_" + preferredName + "Impl";
62+
visibility = Visibility.PACKAGE;
63+
if (attempt > 1) {
64+
name += attempt;
65+
}
66+
}
67+
}
68+
69+
private static Predicate<? super ExecutableElement> matchingErasedParameters(
70+
Types types, TypeMirror... parameterTypes) {
71+
return method -> {
72+
if (method.getParameters().size() != parameterTypes.length) {
73+
return false;
74+
}
75+
for (int i = 0; i < parameterTypes.length; i++) {
76+
VariableElement parameter = method.getParameters().get(i);
77+
TypeMirror erasedType = types.erasure(parameterTypes[i]);
78+
TypeMirror expectedErasedType = types.erasure(parameter.asType());
79+
if (!types.isSameType(erasedType, expectedErasedType)) {
80+
return false;
81+
}
82+
}
83+
return true;
84+
};
85+
}
86+
87+
private static boolean parametersMatchExactly(
88+
Types types, ExecutableElement method, TypeMirror... parameterTypes) {
89+
if (method.getParameters().size() != parameterTypes.length) {
90+
return false;
91+
}
92+
for (int i = 0; i < parameterTypes.length; i++) {
93+
VariableElement parameter = method.getParameters().get(i);
94+
if (!types.isSameType(parameterTypes[i], parameter.asType())) {
95+
return false;
96+
}
97+
}
98+
return true;
99+
}
100+
101+
private static Collector<ExecutableElement, ?, Map<String, ExecutableElement>> byName() {
102+
return toMap(method -> method.getSimpleName().toString(), method -> method);
103+
}
104+
}

0 commit comments

Comments
 (0)