aboutsummaryrefslogtreecommitdiff
path: root/value/src/main/java/com/google/auto
diff options
context:
space:
mode:
authorRon Shapiro <ronshapiro@google.com>2021-03-07 03:15:55 -0800
committerGoogle Java Core Libraries <java-libraries-firehose+copybara@google.com>2021-03-07 03:16:29 -0800
commit9e9be9fa4e6f79a24f801af4ef4fb70dfcd8096f (patch)
treefc6e15c2a317f5debc9dc820d3e1baaa22a95342 /value/src/main/java/com/google/auto
parent8e5f765d74ded371d6cafb0719d2d0ea73d25980 (diff)
downloadauto-9e9be9fa4e6f79a24f801af4ef4fb70dfcd8096f.tar.gz
Implement `@ToPrettyString` to generate pretty `String` versions for AutoValue types.
RELNOTES=Add `@ToPrettyString` for generating pretty `String` versions for AutoValue types. PiperOrigin-RevId: 361402592
Diffstat (limited to 'value/src/main/java/com/google/auto')
-rw-r--r--value/src/main/java/com/google/auto/value/extension/memoized/processor/MemoizeExtension.java2
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java68
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/processor/Annotations.java44
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ClassNames.java25
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ExtensionClassTypeSpecBuilder.java297
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringCollectors.java38
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringExtension.java562
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringMethods.java66
-rw-r--r--value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringValidator.java142
9 files changed, 1244 insertions, 0 deletions
diff --git a/value/src/main/java/com/google/auto/value/extension/memoized/processor/MemoizeExtension.java b/value/src/main/java/com/google/auto/value/extension/memoized/processor/MemoizeExtension.java
index 9b0c8630..50dab429 100644
--- a/value/src/main/java/com/google/auto/value/extension/memoized/processor/MemoizeExtension.java
+++ b/value/src/main/java/com/google/auto/value/extension/memoized/processor/MemoizeExtension.java
@@ -176,6 +176,7 @@ public final class MemoizeExtension extends AutoValueExtension {
return JavaFile.builder(context.packageName(), generated.build()).build().toString();
}
+ // LINT.IfChange
private TypeName superType() {
ClassName superType = ClassName.get(context.packageName(), classToExtend);
ImmutableList<TypeVariableName> typeVariableNames = typeVariableNames();
@@ -251,6 +252,7 @@ public final class MemoizeExtension extends AutoValueExtension {
.build();
}
+ // LINT.IfChange
/**
* True if the given class name is in the com.google.auto.value package or a subpackage. False
* if the class name contains {@code Test}, since many AutoValue tests under
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java
new file mode 100644
index 00000000..cd2762a2
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring;
+
+import static java.lang.annotation.ElementType.METHOD;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Target;
+import java.util.Collection;
+
+/**
+ * Annotates instance methods that return an easy-to-read {@link String} representing the instance.
+ * When the method is {@code abstract} and enclosed in an {@link com.google.auto.value.AutoValue}
+ * class, an implementation of the method will be automatically generated.
+ *
+ * <p>When generating an implementation of an {@code @ToPrettyString} method, each property of the
+ * {@code @AutoValue} type is individually printed in an easy-to-read format. If the type of the
+ * property itself has a {@code @ToPrettyString} method, that method will be called in assistance of
+ * computing the pretty string. Non-{@code @AutoValue} classes can contribute a pretty string
+ * representation by annotating a method with {@code @ToPrettyString}.
+ *
+ * <p>{@link Collection} and {@link Collection}-like types have special representations in generated
+ * pretty strings.
+ *
+ * <p>If no {@code @ToPrettyString} method is found on a type and the type is not one with a built
+ * in rendering, the {@link Object#toString()} value will be used instead.
+ *
+ * <p>{@code @ToPrettyString} is valid on overridden {@code toString()} and other methods alike.
+ *
+ * <h3>Example</h3>
+ *
+ * <pre>
+ * {@code @AutoValue}
+ * abstract class Pretty {
+ * abstract {@code List<String>} property();
+ *
+ * {@code @ToPrettyString}
+ * abstract String toPrettyString();
+ * }
+ *
+ * System.out.println(new AutoValue_Pretty(List.of("abc", "def", "has\nnewline)).toPrettyString())
+ * // Pretty{
+ * // property = [
+ * // abc,
+ * // def,
+ * // has
+ * // newline,
+ * // ]
+ * // }
+ * }</pre>
+ */
+@Documented
+@Target(METHOD)
+public @interface ToPrettyString {}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/Annotations.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/Annotations.java
new file mode 100644
index 00000000..255d6c9c
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/Annotations.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring.processor;
+
+import static com.google.auto.value.extension.toprettystring.processor.ClassNames.TO_PRETTY_STRING_NAME;
+
+import com.google.auto.common.MoreTypes;
+import java.util.Optional;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+
+/** Extension methods for working with {@link AnnotationMirror}. */
+final class Annotations {
+ static Optional<AnnotationMirror> getAnnotationMirror(Element element, String annotationName) {
+ for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
+ TypeElement annotationElement = MoreTypes.asTypeElement(annotation.getAnnotationType());
+ if (annotationElement.getQualifiedName().contentEquals(annotationName)) {
+ return Optional.of(annotation);
+ }
+ }
+ return Optional.empty();
+ }
+
+ static Optional<AnnotationMirror> toPrettyStringAnnotation(Element element) {
+ return getAnnotationMirror(element, TO_PRETTY_STRING_NAME);
+ }
+
+ private Annotations() {}
+}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ClassNames.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ClassNames.java
new file mode 100644
index 00000000..0cfd5772
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ClassNames.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring.processor;
+
+/** Names of classes that are referenced in the processor/extension. */
+final class ClassNames {
+ static final String TO_PRETTY_STRING_NAME =
+ "com.google.auto.value.extension.toprettystring.ToPrettyString";
+
+ private ClassNames() {}
+}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ExtensionClassTypeSpecBuilder.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ExtensionClassTypeSpecBuilder.java
new file mode 100644
index 00000000..528126af
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ExtensionClassTypeSpecBuilder.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring.processor;
+
+import static com.google.auto.common.AnnotationMirrors.getAnnotationValue;
+import static com.google.auto.common.GeneratedAnnotationSpecs.generatedAnnotationSpec;
+import static com.google.auto.common.MoreElements.getPackage;
+import static com.google.auto.common.MoreElements.isAnnotationPresent;
+import static com.google.auto.value.extension.toprettystring.processor.Annotations.getAnnotationMirror;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.Sets.union;
+import static com.squareup.javapoet.MethodSpec.constructorBuilder;
+import static com.squareup.javapoet.TypeSpec.classBuilder;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static javax.lang.model.element.Modifier.ABSTRACT;
+import static javax.lang.model.element.Modifier.FINAL;
+
+import com.google.auto.common.MoreTypes;
+import com.google.auto.common.Visibility;
+import com.google.auto.value.extension.AutoValueExtension;
+import com.google.auto.value.extension.AutoValueExtension.Context;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import com.squareup.javapoet.TypeVariableName;
+import java.lang.annotation.Inherited;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.QualifiedNameable;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * A factory for {@link TypeSpec}s used in {@link AutoValueExtension} implementations.
+ *
+ * <p>This is copied from {@link
+ * com.google.auto.value.extension.memoized.processor.MemoizeExtension} until we find a better
+ * location to consolidate the code.
+ */
+final class ExtensionClassTypeSpecBuilder {
+ private static final String AUTO_VALUE_PACKAGE_NAME = "com.google.auto.value.";
+ private static final String AUTO_VALUE_NAME = AUTO_VALUE_PACKAGE_NAME + "AutoValue";
+ private static final String COPY_ANNOTATIONS_NAME = AUTO_VALUE_NAME + ".CopyAnnotations";
+
+ private final Context context;
+ private final String className;
+ private final String classToExtend;
+ private final boolean isFinal;
+ private final Types types;
+ private final Elements elements;
+ private final SourceVersion sourceVersion;
+
+ private ExtensionClassTypeSpecBuilder(
+ Context context, String className, String classToExtend, boolean isFinal) {
+ this.context = context;
+ this.className = className;
+ this.classToExtend = classToExtend;
+ this.isFinal = isFinal;
+ this.types = context.processingEnvironment().getTypeUtils();
+ this.elements = context.processingEnvironment().getElementUtils();
+ this.sourceVersion = context.processingEnvironment().getSourceVersion();
+ }
+
+ static TypeSpec.Builder extensionClassTypeSpecBuilder(
+ Context context, String className, String classToExtend, boolean isFinal) {
+ return new ExtensionClassTypeSpecBuilder(context, className, classToExtend, isFinal)
+ .extensionClassBuilder();
+ }
+
+ TypeSpec.Builder extensionClassBuilder() {
+ TypeSpec.Builder builder =
+ classBuilder(className)
+ .superclass(superType())
+ .addAnnotations(copiedClassAnnotations(context.autoValueClass()))
+ .addTypeVariables(annotatedTypeVariableNames())
+ .addModifiers(isFinal ? FINAL : ABSTRACT)
+ .addMethod(constructor());
+ generatedAnnotationSpec(elements, sourceVersion, ToPrettyStringExtension.class)
+ .ifPresent(builder::addAnnotation);
+ return builder;
+ }
+
+ private TypeName superType() {
+ ClassName superType = ClassName.get(context.packageName(), classToExtend);
+ ImmutableList<TypeVariableName> typeVariableNames = typeVariableNames();
+
+ return typeVariableNames.isEmpty()
+ ? superType
+ : ParameterizedTypeName.get(superType, typeVariableNames.toArray(new TypeName[] {}));
+ }
+
+ private ImmutableList<TypeVariableName> typeVariableNames() {
+ return context.autoValueClass().getTypeParameters().stream()
+ .map(TypeVariableName::get)
+ .collect(toImmutableList());
+ }
+
+ private ImmutableList<TypeVariableName> annotatedTypeVariableNames() {
+ return context.autoValueClass().getTypeParameters().stream()
+ .map(
+ p ->
+ TypeVariableName.get(p)
+ .annotated(
+ p.getAnnotationMirrors().stream()
+ .map(AnnotationSpec::get)
+ .collect(toImmutableList())))
+ .collect(toImmutableList());
+ }
+
+ private MethodSpec constructor() {
+ MethodSpec.Builder constructor = constructorBuilder();
+ context
+ .propertyTypes()
+ .forEach((name, type) -> constructor.addParameter(annotatedType(type), name + "$"));
+ String superParams =
+ context.properties().keySet().stream().map(n -> n + "$").collect(joining(", "));
+ constructor.addStatement("super($L)", superParams);
+ return constructor.build();
+ }
+
+ /**
+ * True if the given class name is in the com.google.auto.value package or a subpackage. False if
+ * the class name contains {@code Test}, since many AutoValue tests under com.google.auto.value
+ * define their own annotations.
+ */
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private boolean isInAutoValuePackage(String className) {
+ return className.startsWith(AUTO_VALUE_PACKAGE_NAME) && !className.contains("Test");
+ }
+
+ /**
+ * Returns the fully-qualified name of an annotation-mirror, e.g.
+ * "com.google.auto.value.AutoValue".
+ */
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private static String getAnnotationFqName(AnnotationMirror annotation) {
+ return ((QualifiedNameable) annotation.getAnnotationType().asElement())
+ .getQualifiedName()
+ .toString();
+ }
+
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private boolean annotationVisibleFrom(AnnotationMirror annotation, Element from) {
+ Element annotationElement = annotation.getAnnotationType().asElement();
+ Visibility visibility = Visibility.effectiveVisibilityOfElement(annotationElement);
+ switch (visibility) {
+ case PUBLIC:
+ return true;
+ case PROTECTED:
+ // If the annotation is protected, it must be inside another class, call it C. If our
+ // @AutoValue class is Foo then, for the annotation to be visible, either Foo must be in
+ // the same package as C or Foo must be a subclass of C. If the annotation is visible from
+ // Foo then it is also visible from our generated subclass AutoValue_Foo.
+ // The protected case only applies to method annotations. An annotation on the
+ // AutoValue_Foo class itself can't be protected, even if AutoValue_Foo ultimately
+ // inherits from the class that defines the annotation. The JLS says "Access is permitted
+ // only within the body of a subclass":
+ // https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.6.2.1
+ // AutoValue_Foo is a top-level class, so an annotation on it cannot be in the body of a
+ // subclass of anything.
+ return getPackage(annotationElement).equals(getPackage(from))
+ || types.isSubtype(from.asType(), annotationElement.getEnclosingElement().asType());
+ case DEFAULT:
+ return getPackage(annotationElement).equals(getPackage(from));
+ default:
+ return false;
+ }
+ }
+
+ /** Implements the semantics of {@code AutoValue.CopyAnnotations}; see its javadoc. */
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private ImmutableList<AnnotationMirror> annotationsToCopy(
+ Element autoValueType, Element typeOrMethod, Set<String> excludedAnnotations) {
+ ImmutableList.Builder<AnnotationMirror> result = ImmutableList.builder();
+ for (AnnotationMirror annotation : typeOrMethod.getAnnotationMirrors()) {
+ String annotationFqName = getAnnotationFqName(annotation);
+ // To be included, the annotation should not be in com.google.auto.value,
+ // and it should not be in the excludedAnnotations set.
+ if (!isInAutoValuePackage(annotationFqName)
+ && !excludedAnnotations.contains(annotationFqName)
+ && annotationVisibleFrom(annotation, autoValueType)) {
+ result.add(annotation);
+ }
+ }
+
+ return result.build();
+ }
+
+ /** Implements the semantics of {@code AutoValue.CopyAnnotations}; see its javadoc. */
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private ImmutableList<AnnotationSpec> copyAnnotations(
+ Element autoValueType, Element typeOrMethod, Set<String> excludedAnnotations) {
+ ImmutableList<AnnotationMirror> annotationsToCopy =
+ annotationsToCopy(autoValueType, typeOrMethod, excludedAnnotations);
+ return annotationsToCopy.stream().map(AnnotationSpec::get).collect(toImmutableList());
+ }
+
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private static boolean hasAnnotationMirror(Element element, String annotationName) {
+ return getAnnotationMirror(element, annotationName).isPresent();
+ }
+
+ /**
+ * Returns the contents of the {@code AutoValue.CopyAnnotations.exclude} element, as a set of
+ * {@code TypeMirror} where each type is an annotation type.
+ */
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private ImmutableSet<TypeMirror> getExcludedAnnotationTypes(Element element) {
+ Optional<AnnotationMirror> maybeAnnotation =
+ getAnnotationMirror(element, COPY_ANNOTATIONS_NAME);
+ if (!maybeAnnotation.isPresent()) {
+ return ImmutableSet.of();
+ }
+
+ @SuppressWarnings("unchecked")
+ List<AnnotationValue> excludedClasses =
+ (List<AnnotationValue>) getAnnotationValue(maybeAnnotation.get(), "exclude").getValue();
+ return excludedClasses.stream()
+ .map(
+ annotationValue ->
+ MoreTypes.equivalence().wrap((TypeMirror) annotationValue.getValue()))
+ // TODO(b/122509249): Move TypeMirrorSet to common package instead of doing this.
+ .distinct()
+ .map(Wrapper::get)
+ .collect(toImmutableSet());
+ }
+
+ /**
+ * Returns the contents of the {@code AutoValue.CopyAnnotations.exclude} element, as a set of
+ * strings that are fully-qualified class names.
+ */
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private Set<String> getExcludedAnnotationClassNames(Element element) {
+ return getExcludedAnnotationTypes(element).stream()
+ .map(MoreTypes::asTypeElement)
+ .map(typeElement -> typeElement.getQualifiedName().toString())
+ .collect(toSet());
+ }
+
+ // TODO(b/122509249): Move code copied from com.google.auto.value.processor to auto-common.
+ private static Set<String> getAnnotationsMarkedWithInherited(Element element) {
+ return element.getAnnotationMirrors().stream()
+ .filter(a -> isAnnotationPresent(a.getAnnotationType().asElement(), Inherited.class))
+ .map(ExtensionClassTypeSpecBuilder::getAnnotationFqName)
+ .collect(toSet());
+ }
+
+ private ImmutableList<AnnotationSpec> copiedClassAnnotations(TypeElement type) {
+ // Only copy annotations from a class if it has @AutoValue.CopyAnnotations.
+ if (hasAnnotationMirror(type, COPY_ANNOTATIONS_NAME)) {
+ Set<String> excludedAnnotations =
+ union(getExcludedAnnotationClassNames(type), getAnnotationsMarkedWithInherited(type));
+
+ return copyAnnotations(type, type, excludedAnnotations);
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ /** Translate a {@link TypeMirror} into a {@link TypeName}, including type annotations. */
+ private static TypeName annotatedType(TypeMirror type) {
+ List<AnnotationSpec> annotations =
+ type.getAnnotationMirrors().stream().map(AnnotationSpec::get).collect(toList());
+
+ return TypeName.get(type).annotated(annotations);
+ }
+}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringCollectors.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringCollectors.java
new file mode 100644
index 00000000..8fdc1373
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringCollectors.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring.processor;
+
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toCollection;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.LinkedHashSet;
+import java.util.stream.Collector;
+
+final class ToPrettyStringCollectors {
+ static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return collectingAndThen(toList(), ImmutableList::copyOf);
+ }
+
+ static <E> Collector<E, ?, ImmutableSet<E>> toImmutableSet() {
+ return collectingAndThen(toCollection(LinkedHashSet::new), ImmutableSet::copyOf);
+ }
+
+ private ToPrettyStringCollectors() {}
+}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringExtension.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringExtension.java
new file mode 100644
index 00000000..c0f6f736
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringExtension.java
@@ -0,0 +1,562 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring.processor;
+
+import static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
+import static com.google.auto.common.MoreTypes.asTypeElement;
+import static com.google.auto.value.extension.toprettystring.processor.ExtensionClassTypeSpecBuilder.extensionClassTypeSpecBuilder;
+import static com.google.auto.value.extension.toprettystring.processor.ToPrettyStringCollectors.toImmutableList;
+import static com.google.auto.value.extension.toprettystring.processor.ToPrettyStringMethods.toPrettyStringMethod;
+import static com.google.auto.value.extension.toprettystring.processor.ToPrettyStringMethods.toPrettyStringMethods;
+import static com.google.common.collect.Iterables.getLast;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.collect.Sets.intersection;
+import static com.squareup.javapoet.MethodSpec.methodBuilder;
+import static javax.lang.model.element.Modifier.FINAL;
+import static javax.lang.model.element.Modifier.PRIVATE;
+import static javax.lang.model.element.Modifier.PROTECTED;
+import static javax.lang.model.element.Modifier.PUBLIC;
+import static javax.lang.model.element.Modifier.STATIC;
+
+import com.google.auto.common.MoreTypes;
+import com.google.auto.service.AutoService;
+import com.google.auto.value.extension.AutoValueExtension;
+import com.google.auto.value.extension.toprettystring.processor.ToPrettyStringExtension.PrettyPrintableKind.KindVisitor;
+import com.google.common.base.Equivalence;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.SimpleTypeVisitor8;
+import javax.lang.model.util.Types;
+
+/**
+ * Generates implementations of {@link
+ * com.google.auto.value.extension.toprettystring.ToPrettyString} annotated methods in {@link
+ * com.google.auto.value.AutoValue} types.
+ */
+@AutoService(AutoValueExtension.class)
+public final class ToPrettyStringExtension extends AutoValueExtension {
+ private static final ImmutableSet<Modifier> INHERITED_VISIBILITY_MODIFIERS =
+ ImmutableSet.of(PUBLIC, PROTECTED);
+ private static final String INDENT = " ";
+ private static final String INDENT_METHOD_NAME = "$indent";
+ private static final CodeBlock KEY_VALUE_SEPARATOR = CodeBlock.of("$S", ": ");
+
+ @Override
+ public String generateClass(
+ Context context, String className, String classToExtend, boolean isFinal) {
+ TypeSpec type =
+ extensionClassTypeSpecBuilder(context, className, classToExtend, isFinal)
+ .addMethods(toPrettyStringMethodSpecs(context))
+ .build();
+ return JavaFile.builder(context.packageName(), type)
+ .skipJavaLangImports(true)
+ .build()
+ .toString();
+ }
+
+ private ImmutableList<MethodSpec> toPrettyStringMethodSpecs(Context context) {
+ ExecutableElement toPrettyStringMethod = getOnlyElement(toPrettyStringMethods(context));
+ MethodSpec.Builder method =
+ methodBuilder(toPrettyStringMethod.getSimpleName().toString())
+ .addAnnotation(Override.class)
+ .returns(ClassName.get(String.class))
+ .addModifiers(FINAL)
+ .addModifiers(
+ intersection(toPrettyStringMethod.getModifiers(), INHERITED_VISIBILITY_MODIFIERS));
+
+ method.addCode("return $S", context.autoValueClass().getSimpleName() + "{");
+ ToPrettyStringImplementation implementation = ToPrettyStringImplementation.create(context);
+ method.addCode(implementation.toStringCodeBlock.build());
+
+ if (!context.properties().isEmpty()) {
+ method.addCode(" + $S", "\n");
+ }
+ method.addCode(" + $S;\n", "}");
+
+ return ImmutableList.<MethodSpec>builder()
+ .add(method.build())
+ .addAll(implementation.delegateMethods.values())
+ .add(indentMethod())
+ .build();
+ }
+
+ private static MethodSpec indentMethod() {
+ return methodBuilder(INDENT_METHOD_NAME)
+ .addModifiers(PRIVATE, STATIC)
+ .returns(ClassName.get(String.class))
+ .addParameter(TypeName.INT, "level")
+ .addStatement("$1T builder = new $1T()", StringBuilder.class)
+ .beginControlFlow("for (int i = 0; i < level; i++)")
+ .addStatement("builder.append($S)", INDENT)
+ .endControlFlow()
+ .addStatement("return builder.toString()")
+ .build();
+ }
+
+ private static class ToPrettyStringImplementation {
+ private final Types types;
+ private final Elements elements;
+
+ private final CodeBlock.Builder toStringCodeBlock = CodeBlock.builder();
+ private final Map<Equivalence.Wrapper<TypeMirror>, MethodSpec> delegateMethods =
+ new LinkedHashMap<>();
+ private final Set<String> methodNames = new HashSet<>();
+
+ private ToPrettyStringImplementation(Context context) {
+ this.types = context.processingEnvironment().getTypeUtils();
+ this.elements = context.processingEnvironment().getElementUtils();
+ // do not submit: what about "inherited" static methods?
+ getLocalAndInheritedMethods(context.autoValueClass(), types, elements)
+ .forEach(method -> methodNames.add(method.getSimpleName().toString()));
+ }
+
+ static ToPrettyStringImplementation create(Context context) {
+ ToPrettyStringImplementation implemention = new ToPrettyStringImplementation(context);
+ context
+ .propertyTypes()
+ .forEach(
+ (propertyName, type) -> {
+ String methodName =
+ context.properties().get(propertyName).getSimpleName().toString();
+ implemention.toStringCodeBlock.add(
+ "\n + $S + $L + $S",
+ String.format("\n%s%s = ", INDENT, propertyName),
+ implemention.format(CodeBlock.of("$N()", methodName), CodeBlock.of("1"), type),
+ ",");
+ });
+ return implemention;
+ }
+
+ /**
+ * Returns {@code propertyAccess} formatted for use within the {@link
+ * com.google.auto.value.extension.toprettystring.ToPrettyString} implementation.
+ *
+ * <p>If a helper method is necessary for formatting, a {@link MethodSpec} will be added to
+ * {@link #delegateMethods}.
+ *
+ * @param propertyAccess a reference to the variable that should be formatted.
+ * @param indentAccess a reference to an {@code int} representing how many indent levels should
+ * be used for this property.
+ * @param type the type of the {@code propertyAccess}.
+ */
+ private CodeBlock format(CodeBlock propertyAccess, CodeBlock indentAccess, TypeMirror type) {
+ PrettyPrintableKind printableKind = type.accept(new KindVisitor(types, elements), null);
+ DelegateMethod delegateMethod = new DelegateMethod(propertyAccess, indentAccess);
+ switch (printableKind) {
+ case PRIMITIVE:
+ return propertyAccess;
+ case REGULAR_OBJECT:
+ return delegateMethod
+ .methodName("format")
+ .invocation(
+ elements.getTypeElement("java.lang.Object").asType(), () -> reindent("toString"));
+ case HAS_TO_PRETTY_STRING_METHOD:
+ ExecutableElement method =
+ toPrettyStringMethod(asTypeElement(type), types, elements).get();
+ return delegateMethod.invocation(type, () -> reindent(method.getSimpleName()));
+ case ARRAY:
+ TypeMirror componentType = MoreTypes.asArray(type).getComponentType();
+ return delegateMethod.invocation(type, () -> forEachLoopMethodBody(componentType));
+ case COLLECTION:
+ TypeMirror elementType =
+ getOnlyElement(resolvedTypeParameters(type, "java.util.Collection"));
+ return delegateMethod.invocation(
+ collectionOf(elementType), () -> forEachLoopMethodBody(elementType));
+ case IMMUTABLE_PRIMITIVE_ARRAY:
+ return delegateMethod.invocation(type, this::forLoopMethodBody);
+ case OPTIONAL:
+ case GUAVA_OPTIONAL:
+ TypeMirror optionalType = getOnlyElement(MoreTypes.asDeclared(type).getTypeArguments());
+ return delegateMethod.invocation(
+ type, () -> optionalMethodBody(optionalType, printableKind));
+ case MAP:
+ return formatMap(type, delegateMethod);
+ case MULTIMAP:
+ return formatMultimap(type, delegateMethod);
+ }
+ throw new AssertionError(printableKind);
+ }
+
+ private CodeBlock formatMap(TypeMirror type, DelegateMethod delegateMethod) {
+ ImmutableList<TypeMirror> typeParameters = resolvedTypeParameters(type, "java.util.Map");
+ TypeMirror keyType = typeParameters.get(0);
+ TypeMirror valueType = typeParameters.get(1);
+ return delegateMethod.invocation(
+ mapOf(keyType, valueType), () -> mapMethodBody(keyType, valueType));
+ }
+
+ private CodeBlock formatMultimap(TypeMirror type, DelegateMethod delegateMethod) {
+ ImmutableList<TypeMirror> typeParameters =
+ resolvedTypeParameters(type, "com.google.common.collect.Multimap");
+ TypeMirror keyType = typeParameters.get(0);
+ TypeMirror valueType = typeParameters.get(1);
+ return delegateMethod.invocation(
+ multimapOf(keyType, valueType),
+ () -> multimapMethodBody(keyType, collectionOf(valueType)));
+ }
+
+ /**
+ * Parameter object to simplify the branches of {@link #format(CodeBlock, CodeBlock,
+ * TypeMirror)} that call a delegate method.
+ */
+ private class DelegateMethod {
+
+ private final CodeBlock propertyAccess;
+ private final CodeBlock indentAccess;
+ private Optional<String> methodName = Optional.empty();
+
+ DelegateMethod(CodeBlock propertyAccess, CodeBlock indentAccess) {
+ this.propertyAccess = propertyAccess;
+ this.indentAccess = indentAccess;
+ }
+
+ DelegateMethod methodName(String methodName) {
+ this.methodName = Optional.of(methodName);
+ return this;
+ }
+
+ CodeBlock invocation(TypeMirror parameterType, Supplier<CodeBlock> methodBody) {
+ Equivalence.Wrapper<TypeMirror> key = MoreTypes.equivalence().wrap(parameterType);
+ // This doesn't use putIfAbsent because the methodBody supplier could recursively create
+ // new delegate methods. Map.putIfAbsent doesn't support reentrant calls.
+ if (!delegateMethods.containsKey(key)) {
+ delegateMethods.put(
+ key,
+ createMethod(
+ methodName.orElseGet(() -> newDelegateMethodName(parameterType)),
+ parameterType,
+ methodBody));
+ }
+ return CodeBlock.of(
+ "$N($L, $L)", delegateMethods.get(key).name, propertyAccess, indentAccess);
+ }
+
+ private String newDelegateMethodName(TypeMirror type) {
+ String prefix = "format" + nameForType(type);
+ String methodName = prefix;
+ for (int i = 2; !methodNames.add(methodName); i++) {
+ methodName = prefix + i;
+ }
+ return methodName;
+ }
+
+ private MethodSpec createMethod(
+ String methodName, TypeMirror type, Supplier<CodeBlock> methodBody) {
+ return methodBuilder(methodName)
+ .addModifiers(PRIVATE, STATIC)
+ .returns(ClassName.get(String.class))
+ .addParameter(TypeName.get(type), "value")
+ .addParameter(TypeName.INT, "indentLevel")
+ .beginControlFlow("if (value == null)")
+ .addStatement("return $S", "null")
+ .endControlFlow()
+ .addCode(methodBody.get())
+ .build();
+ }
+ }
+
+ private CodeBlock reindent(CharSequence methodName) {
+ return CodeBlock.builder()
+ .addStatement(
+ "return value.$1N().replace($2S, $2S + $3N(indentLevel))",
+ methodName,
+ "\n",
+ INDENT_METHOD_NAME)
+ .build();
+ }
+
+ private CodeBlock forEachLoopMethodBody(TypeMirror elementType) {
+ return loopMethodBody(
+ "[",
+ "]",
+ CodeBlock.of("for ($T element : value)", elementType),
+ format(CodeBlock.of("element"), CodeBlock.of("indentLevel + 1"), elementType));
+ }
+
+ private CodeBlock forLoopMethodBody() {
+ return loopMethodBody(
+ "[",
+ "]",
+ CodeBlock.of("for (int i = 0; i < value.length(); i++)"),
+ CodeBlock.of("value.get(i)"));
+ }
+
+ private CodeBlock mapMethodBody(TypeMirror keyType, TypeMirror valueType) {
+ return forEachMapEntryMethodBody(keyType, valueType, "value");
+ }
+
+ private CodeBlock multimapMethodBody(TypeMirror keyType, TypeMirror valueType) {
+ return forEachMapEntryMethodBody(keyType, valueType, "value.asMap()");
+ }
+
+ private CodeBlock forEachMapEntryMethodBody(
+ TypeMirror keyType, TypeMirror valueType, String propertyAccess) {
+ CodeBlock entryType = CodeBlock.of("$T<$T, $T>", Map.Entry.class, keyType, valueType);
+ return loopMethodBody(
+ "{",
+ "}",
+ CodeBlock.of("for ($L entry : $L.entrySet())", entryType, propertyAccess),
+ format(CodeBlock.of("entry.getKey()"), CodeBlock.of("indentLevel + 1"), keyType),
+ KEY_VALUE_SEPARATOR,
+ format(CodeBlock.of("entry.getValue()"), CodeBlock.of("indentLevel + 1"), valueType));
+ }
+
+ private CodeBlock loopMethodBody(
+ String openSymbol,
+ String closeSymbol,
+ CodeBlock loopDeclaration,
+ CodeBlock... appendedValues) {
+ ImmutableList<CodeBlock> allAppendedValues =
+ ImmutableList.<CodeBlock>builder()
+ .add(CodeBlock.of("$S", "\n"))
+ .add(CodeBlock.of("$N(indentLevel + 1)", INDENT_METHOD_NAME))
+ .add(appendedValues)
+ .add(CodeBlock.of("$S", ","))
+ .build();
+ return CodeBlock.builder()
+ .addStatement("$1T builder = new $1T().append($2S)", StringBuilder.class, openSymbol)
+ .addStatement("boolean hasElements = false")
+ .beginControlFlow("$L", loopDeclaration)
+ .addStatement(
+ "builder$L",
+ allAppendedValues.stream()
+ .map(value -> CodeBlock.of(".append($L)", value))
+ .collect(CodeBlock.joining("")))
+ .addStatement("hasElements = true")
+ .endControlFlow()
+ .beginControlFlow("if (hasElements)")
+ .addStatement("builder.append($S).append($N(indentLevel))", "\n", INDENT_METHOD_NAME)
+ .endControlFlow()
+ .addStatement("return builder.append($S).toString()", closeSymbol)
+ .build();
+ }
+
+ private CodeBlock optionalMethodBody(
+ TypeMirror optionalType, PrettyPrintableKind printableKind) {
+ return CodeBlock.builder()
+ .addStatement(
+ "return (value.isPresent() ? $L : $S)",
+ format(CodeBlock.of("value.get()"), CodeBlock.of("indentLevel"), optionalType),
+ printableKind.equals(PrettyPrintableKind.OPTIONAL) ? "<empty>" : "<absent>")
+ .build();
+ }
+
+ private ImmutableList<TypeMirror> resolvedTypeParameters(
+ TypeMirror propertyType, String interfaceName) {
+ return elements.getTypeElement(interfaceName).getTypeParameters().stream()
+ .map(p -> types.asMemberOf(MoreTypes.asDeclared(propertyType), p))
+ .collect(toImmutableList());
+ }
+
+ private DeclaredType collectionOf(TypeMirror elementType) {
+ return types.getDeclaredType(elements.getTypeElement("java.util.Collection"), elementType);
+ }
+
+ private DeclaredType mapOf(TypeMirror keyType, TypeMirror valueType) {
+ return types.getDeclaredType(elements.getTypeElement("java.util.Map"), keyType, valueType);
+ }
+
+ private DeclaredType multimapOf(TypeMirror keyType, TypeMirror valueType) {
+ return types.getDeclaredType(
+ elements.getTypeElement("com.google.common.collect.Multimap"), keyType, valueType);
+ }
+
+ /** Returns a valid Java identifier for method or variable of type {@code type}. */
+ private String nameForType(TypeMirror type) {
+ return type.accept(
+ new SimpleTypeVisitor8<String, Void>() {
+ @Override
+ public String visitDeclared(DeclaredType type, Void v) {
+ String simpleName = simpleNameForType(type);
+ if (type.getTypeArguments().isEmpty()) {
+ return simpleName;
+ }
+ ImmutableList<String> typeArgumentNames =
+ type.getTypeArguments().stream()
+ .map(t -> simpleNameForType(t))
+ .collect(toImmutableList());
+ if (isMapOrMultimap(type) && typeArgumentNames.size() == 2) {
+ return String.format(
+ "%sOf%sTo%s", simpleName, typeArgumentNames.get(0), typeArgumentNames.get(1));
+ }
+
+ List<String> parts = new ArrayList<>();
+ parts.add(simpleName);
+ parts.add("Of");
+ parts.addAll(typeArgumentNames.subList(0, typeArgumentNames.size() - 1));
+ if (typeArgumentNames.size() > 1) {
+ parts.add("And");
+ }
+ parts.add(getLast(typeArgumentNames));
+ return String.join("", parts);
+ }
+
+ @Override
+ protected String defaultAction(TypeMirror type, Void v) {
+ return simpleNameForType(type);
+ }
+ },
+ null);
+ }
+
+ boolean isMapOrMultimap(TypeMirror type) {
+ TypeMirror mapType = elements.getTypeElement("java.util.Map").asType();
+ if (types.isAssignable(type, types.erasure(mapType))) {
+ return true;
+ }
+ TypeElement multimapElement = elements.getTypeElement("com.google.common.collect.Multimap");
+ return multimapElement != null
+ && types.isAssignable(type, types.erasure(multimapElement.asType()));
+ }
+
+ private String simpleNameForType(TypeMirror type) {
+ return type.accept(
+ new SimpleTypeVisitor8<String, Void>() {
+ @Override
+ public String visitPrimitive(PrimitiveType primitiveType, Void v) {
+ return types.boxedClass(primitiveType).getSimpleName().toString();
+ }
+
+ @Override
+ public String visitArray(ArrayType arrayType, Void v) {
+ return arrayType.getComponentType().accept(this, null) + "Array";
+ }
+
+ @Override
+ public String visitDeclared(DeclaredType declaredType, Void v) {
+ return declaredType.asElement().getSimpleName().toString();
+ }
+
+ @Override
+ protected String defaultAction(TypeMirror typeMirror, Void v) {
+ throw new AssertionError(typeMirror);
+ }
+ },
+ null);
+ }
+ }
+
+ enum PrettyPrintableKind {
+ HAS_TO_PRETTY_STRING_METHOD,
+ REGULAR_OBJECT,
+ PRIMITIVE,
+ COLLECTION,
+ ARRAY,
+ IMMUTABLE_PRIMITIVE_ARRAY,
+ OPTIONAL,
+ GUAVA_OPTIONAL,
+ MAP,
+ MULTIMAP,
+ ;
+
+ private static final ImmutableMap<String, PrettyPrintableKind> KINDS_BY_EXACT_TYPE =
+ ImmutableMap.of(
+ "java.util.Optional", OPTIONAL,
+ "com.google.common.base.Optional", GUAVA_OPTIONAL,
+ "com.google.common.primitives.ImmutableIntArray", IMMUTABLE_PRIMITIVE_ARRAY,
+ "com.google.common.primitives.ImmutableLongArray", IMMUTABLE_PRIMITIVE_ARRAY,
+ "com.google.common.primitives.ImmutableDoubleArray", IMMUTABLE_PRIMITIVE_ARRAY);
+
+ private static final ImmutableMap<String, PrettyPrintableKind> KINDS_BY_SUPERTYPE =
+ ImmutableMap.of(
+ "java.util.Collection", COLLECTION,
+ "java.util.Map", MAP,
+ "com.google.common.collect.Multimap", MULTIMAP);
+
+ static class KindVisitor extends SimpleTypeVisitor8<PrettyPrintableKind, Void> {
+ private final Elements elements;
+ private final Types types;
+
+ KindVisitor(Types types, Elements elements) {
+ this.types = types;
+ this.elements = elements;
+ }
+
+ @Override
+ public PrettyPrintableKind visitPrimitive(PrimitiveType primitiveType, Void v) {
+ return PRIMITIVE;
+ }
+
+ @Override
+ public PrettyPrintableKind visitArray(ArrayType arrayType, Void v) {
+ return ARRAY;
+ }
+
+ @Override
+ public PrettyPrintableKind visitDeclared(DeclaredType declaredType, Void v) {
+ TypeElement typeElement = asTypeElement(declaredType);
+ if (toPrettyStringMethod(typeElement, types, elements).isPresent()) {
+ return HAS_TO_PRETTY_STRING_METHOD;
+ }
+ PrettyPrintableKind byExactType =
+ KINDS_BY_EXACT_TYPE.get(typeElement.getQualifiedName().toString());
+ if (byExactType != null) {
+ return byExactType;
+ }
+
+ for (Map.Entry<String, PrettyPrintableKind> entry : KINDS_BY_SUPERTYPE.entrySet()) {
+ TypeElement supertypeElement = elements.getTypeElement(entry.getKey());
+ if (supertypeElement != null
+ && types.isAssignable(declaredType, types.erasure(supertypeElement.asType()))) {
+ return entry.getValue();
+ }
+ }
+
+ return REGULAR_OBJECT;
+ }
+ }
+ }
+
+ @Override
+ public boolean applicable(Context context) {
+ return toPrettyStringMethods(context).size() == 1;
+ }
+
+ @Override
+ public ImmutableSet<ExecutableElement> consumeMethods(Context context) {
+ return toPrettyStringMethods(context);
+ }
+
+ @Override
+ public IncrementalExtensionType incrementalType(ProcessingEnvironment processingEnvironment) {
+ return IncrementalExtensionType.ISOLATING;
+ }
+}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringMethods.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringMethods.java
new file mode 100644
index 00000000..b5b1242b
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringMethods.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring.processor;
+
+import static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
+import static com.google.auto.value.extension.toprettystring.processor.Annotations.toPrettyStringAnnotation;
+import static com.google.auto.value.extension.toprettystring.processor.ToPrettyStringCollectors.toImmutableList;
+import static com.google.auto.value.extension.toprettystring.processor.ToPrettyStringCollectors.toImmutableSet;
+import static com.google.common.collect.MoreCollectors.toOptional;
+
+import com.google.auto.value.extension.AutoValueExtension.Context;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.Optional;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+final class ToPrettyStringMethods {
+ /**
+ * Returns the {@link com.google.auto.value.extension.toprettystring.ToPrettyString} annotated
+ * methods for an {@code @AutoValue} type.
+ */
+ static ImmutableSet<ExecutableElement> toPrettyStringMethods(Context context) {
+ return context.abstractMethods().stream()
+ .filter(method -> toPrettyStringAnnotation(method).isPresent())
+ .collect(toImmutableSet());
+ }
+
+ /**
+ * Returns the {@link com.google.auto.value.extension.toprettystring.ToPrettyString} annotated
+ * method for a type.
+ */
+ static ImmutableList<ExecutableElement> toPrettyStringMethods(
+ TypeElement element, Types types, Elements elements) {
+ return getLocalAndInheritedMethods(element, types, elements).stream()
+ .filter(method -> toPrettyStringAnnotation(method).isPresent())
+ .collect(toImmutableList());
+ }
+
+ /**
+ * Returns the {@link com.google.auto.value.extension.toprettystring.ToPrettyString} annotated
+ * method for a type.
+ */
+ static Optional<ExecutableElement> toPrettyStringMethod(
+ TypeElement element, Types types, Elements elements) {
+ return toPrettyStringMethods(element, types, elements).stream().collect(toOptional());
+ }
+
+ private ToPrettyStringMethods() {}
+}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringValidator.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringValidator.java
new file mode 100644
index 00000000..b77a54d6
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/processor/ToPrettyStringValidator.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2021 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 com.google.auto.value.extension.toprettystring.processor;
+
+import static com.google.auto.value.extension.toprettystring.processor.ClassNames.TO_PRETTY_STRING_NAME;
+import static com.google.auto.value.extension.toprettystring.processor.ToPrettyStringMethods.toPrettyStringMethods;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toCollection;
+import static javax.lang.model.element.Modifier.STATIC;
+import static javax.lang.model.util.ElementFilter.methodsIn;
+import static javax.tools.Diagnostic.Kind.ERROR;
+
+import com.google.auto.common.MoreElements;
+import com.google.auto.common.MoreTypes;
+import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableList;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import net.ltgt.gradle.incap.IncrementalAnnotationProcessor;
+import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType;
+
+/**
+ * An annotation processor that validates {@link
+ * com.google.auto.value.extension.toprettystring.ToPrettyString} usage.
+ */
+@AutoService(Processor.class)
+@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING)
+@SupportedAnnotationTypes(TO_PRETTY_STRING_NAME)
+public final class ToPrettyStringValidator extends AbstractProcessor {
+ @Override
+ public boolean process(
+ Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
+ Types types = processingEnv.getTypeUtils();
+ Elements elements = processingEnv.getElementUtils();
+ TypeElement toPrettyString = elements.getTypeElement(TO_PRETTY_STRING_NAME);
+
+ Set<ExecutableElement> annotatedMethods =
+ methodsIn(roundEnvironment.getElementsAnnotatedWith(toPrettyString));
+ for (ExecutableElement method : annotatedMethods) {
+ validateMethod(method, elements);
+ }
+
+ validateSingleToPrettyStringMethod(annotatedMethods, types, elements);
+
+ return false;
+ }
+
+ private void validateMethod(ExecutableElement method, Elements elements) {
+ ErrorReporter errorReporter = new ErrorReporter(method, processingEnv.getMessager());
+ if (method.getModifiers().contains(STATIC)) {
+ errorReporter.reportError("@ToPrettyString methods must be instance methods");
+ }
+
+ TypeMirror stringType = elements.getTypeElement("java.lang.String").asType();
+ if (!MoreTypes.equivalence().equivalent(method.getReturnType(), stringType)) {
+ errorReporter.reportError("@ToPrettyString methods must return String");
+ }
+
+ if (!method.getParameters().isEmpty()) {
+ errorReporter.reportError("@ToPrettyString methods cannot have parameters");
+ }
+ }
+
+ private void validateSingleToPrettyStringMethod(
+ Set<ExecutableElement> annotatedMethods, Types types, Elements elements) {
+ Set<TypeElement> enclosingTypes =
+ annotatedMethods.stream()
+ .map(Element::getEnclosingElement)
+ .map(MoreElements::asType)
+ .collect(toCollection(LinkedHashSet::new));
+ for (TypeElement enclosingType : enclosingTypes) {
+ ImmutableList<ExecutableElement> methods =
+ toPrettyStringMethods(enclosingType, types, elements);
+ if (methods.size() > 1) {
+ processingEnv
+ .getMessager()
+ .printMessage(
+ ERROR,
+ String.format(
+ "%s has multiple @ToPrettyString methods:%s",
+ enclosingType.getQualifiedName(), formatMethodList(methods)),
+ enclosingType);
+ }
+ }
+ }
+
+ private String formatMethodList(ImmutableList<ExecutableElement> methods) {
+ return methods.stream().map(this::formatMethodInList).collect(joining());
+ }
+
+ private String formatMethodInList(ExecutableElement method) {
+ return String.format(
+ "\n - %s.%s()",
+ MoreElements.asType(method.getEnclosingElement()).getQualifiedName(),
+ method.getSimpleName());
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
+
+ private static final class ErrorReporter {
+ private final ExecutableElement method;
+ private final Messager messager;
+
+ ErrorReporter(ExecutableElement method, Messager messager) {
+ this.method = method;
+ this.messager = messager;
+ }
+
+ void reportError(String error) {
+ messager.printMessage(ERROR, error, method);
+ }
+ }
+}