Skip to content

Commit

Permalink
Provide a way to control annotations on generated AutoFactory classes.
Browse files Browse the repository at this point in the history
The meta-annotation `@AutoFactory.AnnotationsToApply` specifies an annotation that can be applied alongside `@AutoFactory` to specify the annotations to be applied to the generated class. Its javadoc has more details.

RELNOTES=There is now a way to add annotations to generated AutoFactory classes. See the javadoc for `@AutoFactory.AnnotationsToApply`.
PiperOrigin-RevId: 539648904
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed Jun 12, 2023
1 parent b8a8560 commit b2a1c08
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 2 deletions.
41 changes: 41 additions & 0 deletions factory/src/main/java/com/google/auto/factory/AutoFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.auto.factory;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.TYPE;

Expand Down Expand Up @@ -62,4 +63,44 @@
* Defaults to disallowing subclasses (generating the factory as final).
*/
boolean allowSubclasses() default false;

/**
* Specifies that an annotation should be used to determine how to annotate generated AutoFactory
* classes. For example, suppose you have this annotation:
* <pre>{@code
* @AutoFactory.AnnotationsToApply
* @interface ApplyImmutableAndSuppressWarnings {
* Immutable immutable() default @Immutable;
* SuppressWarnings suppressWarnings() default @SuppressWarnings("Immutable");
* }
* }</pre>
*
* And suppose you use it like this:
* <pre>{@code
* @ApplyImmutableAndSuppressWarnings
* @AutoFactory
* public class Foo {...}
* }</pre>
*
* Then the generated {@code FooFactory} would look like this:
* <pre>{@code
* @Immutable
* @SuppressWarnings("Immutable")
* public class FooFactory {...}
* }</pre>
*
* The same would be true if you used it like this:
* <pre>{@code
* @ApplyImmutableAndSuppressWarnings(
* immutable = @Immutable,
* suppressWarnings = @SuppressWarnings("Immutable"))
* @AutoFactory
* public class Foo {...}
* }</pre>
*
* You could also have {@code suppressWarnings = @SuppressWarnings({"Immutable", "unchecked"})},
* etc, to specify a value different from the default.
*/
@Target(ANNOTATION_TYPE)
@interface AnnotationsToApply {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.google.auto.factory.processor;

import static com.google.auto.common.MoreElements.getPackage;
import static com.google.auto.common.MoreStreams.toImmutableSet;
import static com.google.auto.factory.processor.Elements2.isValidSupertypeForClass;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
Expand All @@ -25,7 +26,9 @@
import static javax.lang.model.util.ElementFilter.typesIn;
import static javax.tools.Diagnostic.Kind.ERROR;

import com.google.auto.common.AnnotationMirrors;
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.AutoFactory.AnnotationsToApply;
import com.google.auto.value.AutoValue;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
Expand Down Expand Up @@ -59,6 +62,7 @@ abstract class AutoFactoryDeclaration {

abstract Optional<String> className();

abstract ImmutableSet<AnnotationMirror> annotations();

abstract TypeElement extendingType();

Expand Down Expand Up @@ -125,6 +129,7 @@ Optional<AutoFactoryDeclaration> createIfValid(Element element) {
return Optional.empty();
}

ImmutableSet<AnnotationMirror> annotations = annotationsToAdd(element);
AnnotationValue extendingValue = checkNotNull(values.get("extending"));
TypeElement extendingType = AnnotationValues.asType(extendingValue);
if (extendingType == null) {
Expand Down Expand Up @@ -187,6 +192,7 @@ public boolean apply(ExecutableElement constructor) {
getAnnotatedType(element),
element,
className.isEmpty() ? Optional.empty() : Optional.of(className),
annotations,
extendingType,
implementingTypes,
allowSubclasses,
Expand All @@ -206,5 +212,35 @@ private static TypeElement getAnnotatedType(Element element) {
static boolean isValidIdentifier(String identifier) {
return SourceVersion.isIdentifier(identifier) && !SourceVersion.isKeyword(identifier);
}

private ImmutableSet<AnnotationMirror> annotationsToAdd(Element element) {
ImmutableSet.Builder<AnnotationMirror> annotationsBuilder = ImmutableSet.builder();

ImmutableSet<? extends AnnotationMirror> containers =
AnnotationMirrors.getAnnotatedAnnotations(element, AnnotationsToApply.class);
switch (containers.size()) {
case 1:
annotationsBuilder.addAll(extractAnnotationsToApply(getOnlyElement(containers)));
break;
case 0:
break;
default:
messager.printMessage(
ERROR, "Multiple @AnnotationsToApply annotations are not supported", element);
}

return annotationsBuilder.build();
}

private ImmutableSet<AnnotationMirror> extractAnnotationsToApply(
AnnotationMirror annotationsToApply) {
return elements.getElementValuesWithDefaults(annotationsToApply).values().stream()
.map(AnnotationValue::getValue)
.filter(AnnotationMirror.class::isInstance)
// Any non-annotation element should already have been flagged when processing
// @AnnotationsToApply
.map(AnnotationMirror.class::cast)
.collect(toImmutableSet());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@
*/
package com.google.auto.factory.processor;

import static com.google.auto.common.MoreTypes.asElement;
import static com.google.auto.common.MoreTypes.asTypeElement;
import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE;
import static javax.lang.model.type.TypeKind.DECLARED;
import static javax.lang.model.type.TypeKind.ERROR;
import static javax.lang.model.util.ElementFilter.methodsIn;

import com.google.auto.common.MoreTypes;
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.AutoFactory.AnnotationsToApply;
import com.google.auto.factory.Provided;
import com.google.auto.service.AutoService;
import com.google.common.base.Throwables;
Expand All @@ -39,6 +47,7 @@
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
Expand Down Expand Up @@ -96,6 +105,11 @@ private void doProcess(RoundEnvironment roundEnv) {
providedChecker.checkProvidedParameter(element);
}

for (Element element :
roundEnv.getElementsAnnotatedWith(AnnotationsToApply.class)) {
checkAnnotationsToApply(element);
}

ImmutableListMultimap.Builder<PackageAndClass, FactoryMethodDescriptor> indexedMethodsBuilder =
ImmutableListMultimap.builder();
ImmutableSetMultimap.Builder<PackageAndClass, ImplementationMethodDescriptor>
Expand Down Expand Up @@ -138,6 +152,9 @@ private void doProcess(RoundEnvironment roundEnv) {
// methodDescriptors.iterator().next() below.
return;
}
// Sort to ensure output is deterministic.
ImmutableSortedSet.Builder<AnnotationMirror> annotationsBuilder =
ImmutableSortedSet.orderedBy(ANNOTATION_COMPARATOR);
// The sets of classes that are mentioned in the `extending` and `implementing`
// elements, respectively, of the @AutoFactory annotations for this factory.
ImmutableSet.Builder<TypeMirror> extending = newTypeSetBuilder();
Expand All @@ -146,6 +163,7 @@ private void doProcess(RoundEnvironment roundEnv) {
Set<Boolean> allowSubclassesSet = new HashSet<>();
boolean skipCreation = false;
for (FactoryMethodDescriptor methodDescriptor : methodDescriptors) {
annotationsBuilder.addAll(methodDescriptor.declaration().annotations());
extending.add(methodDescriptor.declaration().extendingType().asType());
for (TypeElement implementingType :
methodDescriptor.declaration().implementingTypes()) {
Expand All @@ -170,6 +188,7 @@ private void doProcess(RoundEnvironment roundEnv) {
factoryWriter.writeFactory(
FactoryDescriptor.create(
factoryName,
annotationsBuilder.build(),
Iterables.getOnlyElement(extending.build()),
implementing.build(),
publicType,
Expand All @@ -183,6 +202,9 @@ private void doProcess(RoundEnvironment roundEnv) {
});
}

private static final Comparator<AnnotationMirror> ANNOTATION_COMPARATOR =
Comparator.comparing(mirror -> mirror.getAnnotationType().toString());

private ImmutableSet<ImplementationMethodDescriptor> implementationMethods(
TypeElement supertype, Element autoFactoryElement) {
ImmutableSet.Builder<ImplementationMethodDescriptor> implementationMethodsBuilder =
Expand Down Expand Up @@ -234,9 +256,47 @@ private static ImmutableSortedSet.Builder<TypeMirror> newTypeSetBuilder() {
Comparator.comparing(t -> MoreTypes.asTypeElement(t).getQualifiedName().toString()));
}

/** Checks that {@link AnnotationsToApply} is used correctly. */
private void checkAnnotationsToApply(Element annotation) {
if (!annotation.getKind().equals(ANNOTATION_TYPE)) {
// Should not be possible because of @Target.
messager.printMessage(
Kind.ERROR,
"@"
+ AnnotationsToApply.class.getSimpleName()
+ " must be applied to an annotation type declaration.",
annotation);
}
Set<TypeElement> seenAnnotations = new HashSet<>();
for (ExecutableElement annotationMember : methodsIn(annotation.getEnclosedElements())) {
TypeMirror memberType = annotationMember.getReturnType();
boolean isAnnotation = memberType.getKind().equals(DECLARED) && asElement(memberType).getKind().equals(ANNOTATION_TYPE);
if (!isAnnotation && !memberType.getKind().equals(ERROR)) {
messager.printMessage(
Kind.ERROR,
"Members of an @"
+ AnnotationsToApply.class.getSimpleName()
+ " annotation must themselves be annotations; "
+ annotationMember.getSimpleName()
+ " has type "
+ memberType,
annotationMember);
} else {
TypeElement annotationElement = asTypeElement(memberType);
if (!seenAnnotations.add(annotationElement)) {
messager.printMessage(
Kind.ERROR, "More than one @" + annotationElement + " in " + annotation, annotation);
}
}
}
}

@Override
public ImmutableSet<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoFactory.class.getName(), Provided.class.getName());
return ImmutableSet.of(
AutoFactory.class.getCanonicalName(),
Provided.class.getCanonicalName(),
AnnotationsToApply.class.getCanonicalName());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public boolean matches(char c) {

abstract PackageAndClass name();

abstract ImmutableSet<AnnotationMirror> annotations();

abstract TypeMirror extendingType();

Expand Down Expand Up @@ -85,6 +86,7 @@ String getUniqueName(CharSequence base) {

static FactoryDescriptor create(
PackageAndClass name,
ImmutableSet<AnnotationMirror> annotations,
TypeMirror extendingType,
ImmutableSet<TypeMirror> implementingTypes,
boolean publicType,
Expand Down Expand Up @@ -146,6 +148,7 @@ static FactoryDescriptor create(

return new AutoValue_FactoryDescriptor(
name,
annotations,
extendingType,
implementingTypes,
publicType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ void writeFactory(FactoryDescriptor descriptor) throws IOException {
AutoFactoryProcessor.class,
"https://github.com/google/auto/tree/main/factory")
.ifPresent(factory::addAnnotation);
descriptor.annotations().forEach(a -> factory.addAnnotation(AnnotationSpec.get(a)));
if (!descriptor.allowSubclasses()) {
factory.addModifiers(FINAL);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ static boolean isProvider(TypeMirror type) {
}

/**
* Returns an annotation value map with {@link String} keys instead of {@link ExecutableElement}
* Returns an annotation value map with {@link String} keys instead of {@link ExecutableElement}
* instances.
*/
static ImmutableMap<String, AnnotationValue> simplifyAnnotationValueMap(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,48 @@ public void factoryExtendingFinalClass() {
.inFile(file)
.onLineContaining("@AutoFactory");
}

/**
* We don't currently allow you to have more than one {@code @AnnotationsToApply} annotation for
* any given AutoFactory class.
*/
@Test
public void annotationsToApplyMultiple() {
JavaFileObject file = JavaFileObjects.forResource("bad/AnnotationsToApplyMultiple.java");
Compilation compilation = javac.compile(file);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining("Multiple @AnnotationsToApply annotations are not supported");
}

/**
* We also don't allow you to have the same annotation appear more than once inside a given
* {@code @AnnotationsToApply}, even with the same values.
*/
@Test
public void annotationsToApplyRepeated() {
JavaFileObject file = JavaFileObjects.forResource("bad/AnnotationsToApplyRepeated.java");
Compilation compilation = javac.compile(file);
assertThat(compilation).failed();
assertThat(compilation).hadErrorContaining("More than one @java.lang.SuppressWarnings");
}

@Test
public void annotationsToApplyNotAnnotations() {
JavaFileObject file = JavaFileObjects.forResource("bad/AnnotationsToApplyNotAnnotations.java");
Compilation compilation = javac.compile(file);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
"Members of an @AnnotationsToApply annotation must themselves be annotations;"
+ " whatIsThis has type int")
.inFile(file)
.onLineContaining("whatIsThis");
assertThat(compilation)
.hadErrorContaining(
"Members of an @AnnotationsToApply annotation must themselves be annotations;"
+ " andWhatIsThis has type com.google.errorprone.annotations.Immutable[]")
.inFile(file)
.onLineContaining("andWhatIsThis");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ private enum Config {
static final ImmutableList<File> COMMON_CLASSPATH =
ImmutableList.of(
fileForClass("com.google.auto.factory.AutoFactory"),
fileForClass("com.google.errorprone.annotations.Immutable"),
fileForClass("javax.annotation.Nullable"),
fileForClass("org.checkerframework.checker.nullness.compatqual.NullableType"));
static final File JAVAX_CLASSPATH = fileForClass("javax.inject.Provider");
Expand Down Expand Up @@ -640,6 +641,14 @@ public void parameterAnnotations() {
"tests.ParameterAnnotationsFactory", "expected/ParameterAnnotationsFactory.java"));
}

@Test
public void customAnnotations() {
goldenTest(
ImmutableList.of("good/CustomAnnotations.java"),
ImmutableMap.of(
"tests.CustomAnnotationsFactory", "expected/CustomAnnotationsFactory.java"));
}

private JavaFileObject loadExpectedFile(String resourceName) {
try {
List<String> sourceLines = Resources.readLines(Resources.getResource(resourceName), UTF_8);
Expand Down
33 changes: 33 additions & 0 deletions factory/src/test/resources/bad/AnnotationsToApplyMultiple.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tests;

import com.google.auto.factory.AutoFactory;

@AutoFactory.AnnotationsToApply
@interface This {
SuppressWarnings suppressWarnings() default @SuppressWarnings("Immutable");
}

@AutoFactory.AnnotationsToApply
@interface That {
SuppressWarnings suppressWarnings() default @SuppressWarnings("Immutable");
}

@This
@That
@AutoFactory
final class AnnotationsToApplyMultiple {}
Loading

0 comments on commit b2a1c08

Please sign in to comment.