Skip to content

Commit

Permalink
Adds support for extending AutoValue.Builder via abstract methods.
Browse files Browse the repository at this point in the history
Leveraging AutoValueExtension to extend a class didn't allow extending the AutoValue.Builder class via abstract methods. This change allows extensions to consume abstract methods in the builder so that they can provide their own implementations.

RELNOTES=Added support for extending AutoValue.Builder with abstract methods.
PiperOrigin-RevId: 565160145
  • Loading branch information
java-team-github-bot authored and Google Java Core Libraries committed Sep 13, 2023
1 parent fe2eb81 commit 7d4b020
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
* compiler's {@code -classpath} or {@code -processorpath}.
*
* <p>When the AutoValue processor runs for a class {@code Foo}, it will ask each Extension whether
* it is {@linkplain #applicable applicable}. Suppose two Extensions reply that they are. Then
* the processor will generate the AutoValue logic in a direct subclass of {@code Foo}, and it
* will ask the first Extension to generate a subclass of that, and the second Extension to generate
* a subclass of the subclass. So we might have this hierarchy:
* it is {@linkplain #applicable applicable}. Suppose two Extensions reply that they are. Then the
* processor will generate the AutoValue logic in a direct subclass of {@code Foo}, and it will ask
* the first Extension to generate a subclass of that, and the second Extension to generate a
* subclass of the subclass. So we might have this hierarchy:
*
* <pre>
* &#64;AutoValue abstract class Foo {...} // the hand-written class
Expand All @@ -63,17 +63,17 @@
* <p>The first generated class in the hierarchy will always be the one generated by the AutoValue
* processor and the last one will always be the one generated by the Extension that {@code
* mustBeFinal}, if any. Other than that, the order of the classes in the hierarchy is unspecified.
* The last class in the hierarchy is {@code AutoValue_Foo} and that is the one that the
* {@code Foo} class will reference, for example with {@code new AutoValue_Foo(...)}.
* The last class in the hierarchy is {@code AutoValue_Foo} and that is the one that the {@code Foo}
* class will reference, for example with {@code new AutoValue_Foo(...)}.
*
* <p>Each Extension must also be sure to generate a constructor with arguments corresponding to all
* properties in {@link com.google.auto.value.extension.AutoValueExtension.Context#propertyTypes()},
* in order, and to call the superclass constructor with the same arguments. This constructor must
* have at least package visibility.
*
* <p>Because the class generated by the AutoValue processor is at the top of the generated
* hierarchy, Extensions can override its methods, for example {@code hashCode()},
* {@code toString()}, or the implementations of the various {@code bar()} property methods.
* hierarchy, Extensions can override its methods, for example {@code hashCode()}, {@code
* toString()}, or the implementations of the various {@code bar()} property methods.
*/
public abstract class AutoValueExtension {

Expand All @@ -99,10 +99,10 @@ public interface Context {

/**
* The fully-qualified name of the last class in the {@code AutoValue} hierarchy. For an
* {@code @AutoValue} class {@code foo.bar.Baz}, this will be {@code foo.bar.AutoValue_Baz}.
* The class may be generated by an extension, which will be the current extension if the
* {@code isFinal} parameter to {@link AutoValueExtension#generateClass} is true and the
* returned string is not {@code null}.
* {@code @AutoValue} class {@code foo.bar.Baz}, this will be {@code foo.bar.AutoValue_Baz}. The
* class may be generated by an extension, which will be the current extension if the {@code
* isFinal} parameter to {@link AutoValueExtension#generateClass} is true and the returned
* string is not {@code null}.
*
* <p>For compatibility reasons, this method has a default implementation that throws an
* exception. The AutoValue processor supplies an implementation that behaves as documented.
Expand Down Expand Up @@ -159,6 +159,14 @@ default Map<String, TypeMirror> propertyTypes() {
*/
Set<ExecutableElement> abstractMethods();

/**
* Returns the complete set of abstract methods defined in or inherited by the {@code @Builder}
* class. This includes methods that have been consumed by this or another Extension.
*
* <p>If there is no builder class, then this set is empty.
*/
Set<ExecutableElement> builderAbstractMethods();

/**
* Returns the complete list of annotations defined on the {@code classToCopyFrom} that should
* be added to any generated subclass. Only annotations visible to the {@code @AutoValue} will
Expand Down Expand Up @@ -203,9 +211,7 @@ default Optional<BuilderContext> builder() {
}
}

/**
* Represents a {@code Builder} associated with an {@code @AutoValue} class.
*/
/** Represents a {@code Builder} associated with an {@code @AutoValue} class. */
public interface BuilderContext {
/**
* Returns the {@code @AutoValue.Builder} interface or abstract class that this object
Expand All @@ -218,6 +224,7 @@ public interface BuilderContext {
* type.
*
* <p>Consider a class like this:
*
* <pre>
* {@code @AutoValue} abstract class Foo {
* abstract String bar();
Expand All @@ -230,8 +237,8 @@ public interface BuilderContext {
* }
* </pre>
*
* <p>Here {@code toBuilderMethods()} will return a set containing the method
* {@code Foo.toBuilder()}.
* <p>Here {@code toBuilderMethods()} will return a set containing the method {@code
* Foo.toBuilder()}.
*/
Set<ExecutableElement> toBuilderMethods();

Expand All @@ -240,6 +247,7 @@ public interface BuilderContext {
* type.
*
* <p>Consider a class like this:
*
* <pre>
* {@code @AutoValue} abstract class Foo {
* abstract String bar();
Expand All @@ -257,19 +265,19 @@ public interface BuilderContext {
* }
* </pre>
*
* <p>Here {@code builderMethods()} will return a set containing the method
* {@code Foo.builder()}. Generated code should usually call this method in preference to
* constructing {@code AutoValue_Foo.Builder()} directly, because this method can establish
* default values for properties, as it does here.
* <p>Here {@code builderMethods()} will return a set containing the method {@code
* Foo.builder()}. Generated code should usually call this method in preference to constructing
* {@code AutoValue_Foo.Builder()} directly, because this method can establish default values
* for properties, as it does here.
*/
Set<ExecutableElement> builderMethods();

/**
* Returns the method {@code build()} in the builder class, if it exists and returns the
* {@code @AutoValue} type. This is the method that generated code for
* {@code @AutoValue class Foo} should call in order to get an instance of {@code Foo} from its
* builder. The returned method is called {@code build()}; if the builder uses some other name
* then extensions have no good way to guess how they should build.
* {@code @AutoValue} type. This is the method that generated code for {@code @AutoValue class
* Foo} should call in order to get an instance of {@code Foo} from its builder. The returned
* method is called {@code build()}; if the builder uses some other name then extensions have no
* good way to guess how they should build.
*
* <p>A common convention is for {@code build()} to be a concrete method in the
* {@code @AutoValue.Builder} class, which calls an abstract method {@code autoBuild()} that is
Expand All @@ -281,9 +289,9 @@ public interface BuilderContext {
/**
* Returns the abstract build method. If the {@code @AutoValue} class is {@code Foo}, this is an
* abstract no-argument method in the builder class that returns {@code Foo}. This might be
* called {@code build()}, or, following a common convention, it might be called
* {@code autoBuild()} and used in the implementation of a {@code build()} method that is
* defined in the builder class.
* called {@code build()}, or, following a common convention, it might be called {@code
* autoBuild()} and used in the implementation of a {@code build()} method that is defined in
* the builder class.
*
* <p>Extensions should call the {@code build()} method in preference to this one. But they
* should override this one if they want to customize build-time behaviour.
Expand All @@ -292,19 +300,18 @@ public interface BuilderContext {

/**
* Returns a map from property names to the corresponding setters. A property may have more than
* one setter. For example, an {@code ImmutableList<String>} might be set by
* {@code setFoo(ImmutableList<String>)} and {@code setFoo(String[])}.
* one setter. For example, an {@code ImmutableList<String>} might be set by {@code
* setFoo(ImmutableList<String>)} and {@code setFoo(String[])}.
*/
Map<String, Set<ExecutableElement>> setters();

/**
* Returns a map from property names to property builders. For example, if there is a property
* {@code foo} defined by {@code abstract ImmutableList<String> foo();} or
* {@code abstract ImmutableList<String> getFoo();} in the {@code @AutoValue} class,
* then there can potentially be a builder defined by
* {@code abstract ImmutableList.Builder<String> fooBuilder();} in the
* {@code @AutoValue.Builder} class. This map would then map {@code "foo"} to the
* {@link ExecutableElement} representing {@code fooBuilder()}.
* {@code foo} defined by {@code abstract ImmutableList<String> foo();} or {@code abstract
* ImmutableList<String> getFoo();} in the {@code @AutoValue} class, then there can potentially
* be a builder defined by {@code abstract ImmutableList.Builder<String> fooBuilder();} in the
* {@code @AutoValue.Builder} class. This map would then map {@code "foo"} to the {@link
* ExecutableElement} representing {@code fooBuilder()}.
*/
Map<String, ExecutableElement> propertyBuilders();
}
Expand All @@ -328,8 +335,8 @@ public enum IncrementalExtensionType {

/**
* This extension is <i>aggregating</i>, meaning that it may generate outputs based on several
* annotated input classes and it respects the constraints imposed on aggregating processors.
* It is unusual for AutoValue extensions to be aggregating.
* annotated input classes and it respects the constraints imposed on aggregating processors. It
* is unusual for AutoValue extensions to be aggregating.
*
* @see <a
* href="https://docs.gradle.org/current/userguide/java_plugin.html#aggregating_annotation_processors">Gradle
Expand Down Expand Up @@ -436,10 +443,10 @@ public Set<String> consumeProperties(Context context) {
}

/**
* Returns a possible empty set of abstract methods that this Extension intends to implement. This
* Returns a possibly empty set of abstract methods that this Extension intends to implement. This
* will prevent AutoValue from generating an implementation, in cases where it would have, and it
* will also avoid warnings about abstract methods that AutoValue doesn't expect. The default set
* returned by this method is empty.
* will also avoid complaints about abstract methods that AutoValue doesn't expect. The default
* set returned by this method is empty.
*
* <p>Each returned method must be one of the abstract methods in {@link
* Context#abstractMethods()}.
Expand All @@ -457,6 +464,21 @@ public Set<ExecutableElement> consumeMethods(Context context) {
return ImmutableSet.of();
}

/**
* Returns a possibly empty set of abstract methods that this Extension intends to implement. This
* will prevent AutoValue from generating an implementation, in cases where it would have, and it
* will also avoid complaints about abstract methods that AutoValue doesn't expect. The default
* set returned by this method is empty.
*
* <p>Each returned method must be one of the abstract methods in {@link
* Context#builderAbstractMethods()}.
*
* @param context the Context of the code generation for this class.
*/
public Set<ExecutableElement> consumeBuilderMethods(Context context) {
return ImmutableSet.of();
}

/**
* Returns the generated source code of the class named {@code className} to extend {@code
* classToExtend}, or {@code null} if this extension does not generate a class in the hierarchy.
Expand All @@ -476,7 +498,8 @@ public Set<ExecutableElement> consumeMethods(Context context) {
* ...
* }
* ...
* }}</pre>
* }
* }</pre>
*
* <p>Here, {@code <package>} is {@link Context#packageName()}; {@code <finalOrAbstract>} is the
* keyword {@code final} if {@code isFinal} is true or {@code abstract} otherwise; and {@code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,10 @@ abstract class AutoValueOrBuilderTemplateVars extends AutoValueishTemplateVars {
* subclasses)
*/
Boolean isFinal = false;

/**
* The modifiers (for example {@code final} or {@code abstract}) for the generated builder
* subclass, followed by a space if they are not empty.
*/
String builderClassModifiers = "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,13 @@ void processType(TypeElement type) {
BuilderSpec builderSpec = new BuilderSpec(type, processingEnv, errorReporter());
Optional<BuilderSpec.Builder> builder = builderSpec.getBuilder();
ImmutableSet<ExecutableElement> toBuilderMethods;
ImmutableSet<ExecutableElement> builderAbstractMethods;
if (builder.isPresent()) {
toBuilderMethods = builder.get().toBuilderMethods(typeUtils(), type, abstractMethods);
builderAbstractMethods = builder.get().builderAbstractMethods();
} else {
toBuilderMethods = ImmutableSet.of();
builderAbstractMethods = ImmutableSet.of();
}

ImmutableMap<ExecutableElement, TypeMirror> propertyMethodsAndTypes =
Expand All @@ -219,11 +222,19 @@ void processType(TypeElement type) {

ExtensionContext context =
new ExtensionContext(
processingEnv, type, properties, propertyMethodsAndTypes, abstractMethods);
processingEnv,
type,
properties,
propertyMethodsAndTypes,
abstractMethods,
builderAbstractMethods);
ImmutableList<AutoValueExtension> applicableExtensions = applicableExtensions(type, context);
ImmutableSet<ExecutableElement> consumedMethods =
methodsConsumedByExtensions(
type, applicableExtensions, context, abstractMethods, properties);
ImmutableSet<ExecutableElement> consumedBuilderMethods =
builderMethodsConsumedByExtensions(
type, applicableExtensions, context, builderAbstractMethods);

if (!consumedMethods.isEmpty()) {
ImmutableSet<ExecutableElement> allAbstractMethods = abstractMethods;
Expand All @@ -234,7 +245,12 @@ void processType(TypeElement type) {
properties = propertyNameToMethodMap(propertyMethodsAndTypes.keySet());
context =
new ExtensionContext(
processingEnv, type, properties, propertyMethodsAndTypes, allAbstractMethods);
processingEnv,
type,
properties,
propertyMethodsAndTypes,
allAbstractMethods,
builderAbstractMethods);
}

ImmutableSet<ExecutableElement> propertyMethods = propertyMethodsAndTypes.keySet();
Expand All @@ -252,7 +268,8 @@ void processType(TypeElement type) {
toBuilderMethods,
propertyMethodsAndTypes,
builder,
nullables);
nullables,
consumedBuilderMethods);
vars.builtType = vars.origClass + vars.actualTypes;
vars.build = "new " + finalSubclass + vars.actualTypes;

Expand All @@ -270,6 +287,10 @@ void processType(TypeElement type) {
vars.subclass = TypeSimplifier.simpleNameOf(subclass);
vars.isFinal = (subclassDepth == 0);
vars.modifiers = vars.isFinal ? "final " : "abstract ";
vars.builderClassModifiers =
consumedBuilderMethods.isEmpty()
? vars.isFinal ? "static final " : "static "
: "abstract static ";

String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, vars.pkg, type.asType());
Expand Down Expand Up @@ -393,6 +414,40 @@ private ImmutableSet<ExecutableElement> methodsConsumedByExtensions(
return ImmutableSet.copyOf(consumed);
}

private ImmutableSet<ExecutableElement> builderMethodsConsumedByExtensions(
TypeElement type,
ImmutableList<AutoValueExtension> applicableExtensions,
ExtensionContext context,
ImmutableSet<ExecutableElement> builderAbstractMethods) {
Set<ExecutableElement> consumed = new HashSet<>();
for (AutoValueExtension extension : applicableExtensions) {
Set<ExecutableElement> consumedHere = new HashSet<>();
for (ExecutableElement consumedMethod : extension.consumeBuilderMethods(context)) {
if (!builderAbstractMethods.contains(consumedMethod)) {
errorReporter()
.reportError(
type,
"[AutoValueBuilderConsumeNotAbstract] Extension %s wants to consume a method that"
+ " is not one of the abstract methods in this class: %s",
extensionName(extension),
consumedMethod);
} else {
consumedHere.add(consumedMethod);
}
}
for (ExecutableElement repeat : intersection(consumed, consumedHere)) {
errorReporter()
.reportError(
repeat,
"[AutoValueBuilderConsumeNotAbstract] Extension %s wants to consume a method that"
+ " was already consumed by another extension",
extensionName(extension));
}
consumed.addAll(consumedHere);
}
return ImmutableSet.copyOf(consumed);
}

private void validateMethods(
TypeElement type,
ImmutableSet<ExecutableElement> abstractMethods,
Expand Down Expand Up @@ -430,7 +485,8 @@ private void defineVarsForType(
ImmutableSet<ExecutableElement> toBuilderMethods,
ImmutableMap<ExecutableElement, TypeMirror> propertyMethodsAndTypes,
Optional<BuilderSpec.Builder> maybeBuilder,
Nullables nullables) {
Nullables nullables,
ImmutableSet<ExecutableElement> consumedBuilderAbstractMethods) {
ImmutableSet<ExecutableElement> propertyMethods = propertyMethodsAndTypes.keySet();
vars.toBuilderMethods =
toBuilderMethods.stream().map(SimpleMethod::new).collect(toImmutableList());
Expand All @@ -450,7 +506,8 @@ private void defineVarsForType(
builder -> {
ImmutableBiMap<ExecutableElement, String> methodToPropertyName =
propertyNameToMethodMap(propertyMethods).inverse();
builder.defineVarsForAutoValue(vars, methodToPropertyName, nullables);
builder.defineVarsForAutoValue(
vars, methodToPropertyName, nullables, consumedBuilderAbstractMethods);
vars.builderName = "Builder";
vars.builderAnnotations = copiedClassAnnotations(builder.builderType());
});
Expand Down
Loading

0 comments on commit 7d4b020

Please sign in to comment.