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

Commit c247b0b

Browse files
authored
#283 Fix toBuilder() issues
Generate Partial.toBuilder whenever a package-visible no-args constructor exists, and issue a compiler error if one doesn't
2 parents 8b3f86d + 09f10f5 commit c247b0b

17 files changed

+448
-182
lines changed

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ public interface Person {
111111
```
112112

113113
The `toBuilder()` method here is optional but highly recommended.
114-
If you are writing an abstract class, or using Java 8, you may wish to hide the
115-
builder's constructor and manually provide instead a static `builder()` method
116-
on the value type (though <em>Effective Java</em> does not do this).
114+
If you are writing an abstract class, or using Java 8, you may wish to make the
115+
builder's constructor package-protected and manually provide instead a static
116+
`builder()` method on the value type (though <em>Effective Java</em> does not do
117+
this).
117118

118119

119120
### What you get

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

+15-11
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ Metadata analyse(TypeElement type) throws CannotGenerateCodeException {
140140
QualifiedName generatedBuilder = QualifiedName.of(
141141
pkg.getQualifiedName().toString(), generatedBuilderSimpleName(type));
142142
Optional<TypeElement> builder = tryFindBuilder(generatedBuilder, type);
143-
Optional<BuilderFactory> builderFactory = builderFactory(builder);
143+
Metadata.Builder constructionAndExtension = constructionAndExtension(builder);
144144
QualifiedName valueType = generatedBuilder.nestedType("Value");
145145
QualifiedName partialType = generatedBuilder.nestedType("Partial");
146146
QualifiedName propertyType = generatedBuilder.nestedType("Property");
@@ -151,7 +151,7 @@ Metadata analyse(TypeElement type) throws CannotGenerateCodeException {
151151
.setType(QualifiedName.of(type).withParameters(typeParameters))
152152
.setInterfaceType(type.getKind().isInterface())
153153
.setBuilder(parameterized(builder, typeParameters))
154-
.setBuilderFactory(builderFactory)
154+
.mergeFrom(constructionAndExtension)
155155
.setGeneratedBuilder(generatedBuilder.withParameters(typeParameters))
156156
.setValueType(valueType.withParameters(typeParameters))
157157
.setPartialType(partialType.withParameters(typeParameters))
@@ -161,7 +161,8 @@ Metadata analyse(TypeElement type) throws CannotGenerateCodeException {
161161
.addVisibleNestedTypes(propertyType)
162162
.addAllVisibleNestedTypes(visibleTypesIn(type)) // Because we inherit from type
163163
.putAllStandardMethodUnderrides(findUnderriddenMethods(methods))
164-
.setHasToBuilderMethod(hasToBuilderMethod(builder, builderFactory, methods))
164+
.setHasToBuilderMethod(hasToBuilderMethod(
165+
builder, constructionAndExtension.isExtensible(), methods))
165166
.setBuilderSerializable(shouldBuilderBeSerializable(builder))
166167
.addAllProperties(properties.values());
167168
Metadata baseMetadata = metadataBuilder.build();
@@ -310,18 +311,17 @@ private Map<StandardMethod, UnderrideLevel> findUnderriddenMethods(
310311
return result.build();
311312
}
312313

313-
/** Find a toBuilder method, if the user has provided one.
314-
* @param builderFactory */
314+
/** Find a toBuilder method, if the user has provided one. */
315315
private boolean hasToBuilderMethod(
316316
Optional<TypeElement> builder,
317-
Optional<BuilderFactory> builderFactory,
317+
boolean isExtensible,
318318
Iterable<ExecutableElement> methods) {
319319
if (!builder.isPresent()) {
320320
return false;
321321
}
322322
for (ExecutableElement method : methods) {
323323
if (isToBuilderMethod(builder.get(), method)) {
324-
if (!builderFactory.isPresent()) {
324+
if (!isExtensible) {
325325
messager.printMessage(ERROR,
326326
"No accessible no-args Builder constructor available to implement toBuilder",
327327
method);
@@ -408,15 +408,19 @@ private Optional<TypeElement> tryFindBuilder(
408408
return userClass;
409409
}
410410

411-
private Optional<BuilderFactory> builderFactory(Optional<TypeElement> builder) {
411+
private Metadata.Builder constructionAndExtension(Optional<TypeElement> builder) {
412412
if (!builder.isPresent()) {
413-
return Optional.of(NO_ARGS_CONSTRUCTOR);
413+
return new Metadata.Builder()
414+
.setExtensible(false)
415+
.setBuilderFactory(NO_ARGS_CONSTRUCTOR);
414416
}
415417
if (!builder.get().getModifiers().contains(Modifier.STATIC)) {
416418
messager.printMessage(ERROR, "Builder must be static on @FreeBuilder types", builder.get());
417-
return Optional.absent();
419+
return new Metadata.Builder().setExtensible(false);
418420
}
419-
return BuilderFactory.from(builder.get());
421+
return new Metadata.Builder()
422+
.setExtensible(BuilderFactory.hasNoArgsConstructor(builder.get()))
423+
.setBuilderFactory(BuilderFactory.from(builder.get()));
420424
}
421425

422426
private Map<ExecutableElement, Property> findProperties(

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public static Optional<BuilderFactory> from(TypeElement builderType) {
104104
return Optional.of(BUILDER_METHOD);
105105
} else if (staticMethods.contains("newBuilder")) {
106106
return Optional.of(NEW_BUILDER_METHOD);
107-
} else if (hasExplicitNoArgsConstructor(builderType)) {
107+
} else if (hasNoArgsConstructor(builderType)) {
108108
return Optional.of(NO_ARGS_CONSTRUCTOR);
109109
} else {
110110
return Optional.absent();
@@ -118,7 +118,9 @@ private static boolean typeIsAbstract(TypeElement type) {
118118
return type.getModifiers().contains(Modifier.ABSTRACT);
119119
}
120120

121-
private static boolean hasExplicitNoArgsConstructor(TypeElement type) {
121+
static boolean hasNoArgsConstructor(TypeElement type) {
122+
// A type with no explictly-declared constructors will still have an implicit constructor in its
123+
// enclosed elements.
122124
for (ExecutableElement constructor : constructorsIn(type.getEnclosedElements())) {
123125
if (constructor.getParameters().isEmpty()
124126
&& !constructor.getModifiers().contains(Modifier.PRIVATE)) {

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -643,8 +643,7 @@ private static void addPartialToBuilderMethod(SourceBuilder code, Metadata metad
643643
if (!metadata.getHasToBuilderMethod()) {
644644
return;
645645
}
646-
BuilderFactory builderFactory = metadata.getBuilderFactory().orNull();
647-
if (builderFactory == BuilderFactory.NO_ARGS_CONSTRUCTOR) {
646+
if (metadata.isExtensible()) {
648647
code.addLine("")
649648
.addLine(" private static class PartialBuilder%s extends %s {",
650649
metadata.getBuilder().declarationParameters(), metadata.getBuilder())
@@ -656,7 +655,7 @@ private static void addPartialToBuilderMethod(SourceBuilder code, Metadata metad
656655
code.addLine("")
657656
.addLine(" @%s", Override.class)
658657
.addLine(" public %s toBuilder() {", metadata.getBuilder());
659-
if (builderFactory == BuilderFactory.NO_ARGS_CONSTRUCTOR) {
658+
if (metadata.isExtensible()) {
660659
code.addLine(" %s builder = new PartialBuilder%s();",
661660
metadata.getBuilder(), metadata.getBuilder().typeParametersOrDiamondOperator());
662661
Block block = new Block(code);

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

+3
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ public ParameterizedType getBuilder() {
112112
return getOptionalBuilder().get();
113113
}
114114

115+
/** Whether there is a package-visible, no-args constructor so we can subclass the Builder. */
116+
public abstract boolean isExtensible();
117+
115118
/** Returns the builder factory mechanism the user has exposed, if any. */
116119
public abstract Optional<BuilderFactory> getBuilderFactory();
117120

0 commit comments

Comments
 (0)