aboutsummaryrefslogtreecommitdiff
path: root/value/src
diff options
context:
space:
mode:
Diffstat (limited to 'value/src')
-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
-rw-r--r--value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringTest.java961
-rw-r--r--value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringValidatorTest.java240
-rw-r--r--value/src/test/java/com/google/auto/value/processor/IncrementalExtensionTest.java6
12 files changed, 2450 insertions, 1 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);
+ }
+ }
+}
diff --git a/value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringTest.java b/value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringTest.java
new file mode 100644
index 00000000..6388966f
--- /dev/null
+++ b/value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringTest.java
@@ -0,0 +1,961 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.toprettystring.ToPrettyStringTest.CollectionSubtypesWithFixedTypeParameters.StringList;
+import com.google.auto.value.extension.toprettystring.ToPrettyStringTest.CollectionSubtypesWithFixedTypeParameters.StringMap;
+import com.google.auto.value.extension.toprettystring.ToPrettyStringTest.PropertyHasToPrettyString.HasToPrettyString;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.primitives.ImmutableIntArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SuppressWarnings("AutoValueImmutableFields")
+@RunWith(JUnit4.class)
+public class ToPrettyStringTest {
+ @AutoValue
+ abstract static class Primitives {
+ abstract int i();
+
+ abstract long l();
+
+ abstract byte b();
+
+ abstract short s();
+
+ abstract char c();
+
+ abstract float f();
+
+ abstract double d();
+
+ abstract boolean bool();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void primitives() {
+ Primitives valueType =
+ new AutoValue_ToPrettyStringTest_Primitives(
+ 1, 2L, (byte) 3, (short) 4, 'C', 6.6f, 7.7, false);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "Primitives{"
+ + "\n i = 1,"
+ + "\n l = 2,"
+ + "\n b = 3,"
+ + "\n s = 4,"
+ + "\n c = C,"
+ + "\n f = 6.6,"
+ + "\n d = 7.7,"
+ + "\n bool = false,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class PrimitiveArray {
+ @Nullable
+ @SuppressWarnings("mutable")
+ abstract long[] longs();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void primitiveArray() {
+ PrimitiveArray valueType =
+ new AutoValue_ToPrettyStringTest_PrimitiveArray(new long[] {1L, 2L, 10L, 200L});
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrimitiveArray{"
+ + "\n longs = ["
+ + "\n 1,"
+ + "\n 2,"
+ + "\n 10,"
+ + "\n 200,"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @Test
+ public void primitiveArray_empty() {
+ PrimitiveArray valueType = new AutoValue_ToPrettyStringTest_PrimitiveArray(new long[0]);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrimitiveArray{" // force newline
+ + "\n longs = [],"
+ + "\n}");
+ }
+
+ @Test
+ public void primitiveArray_null() {
+ PrimitiveArray valueType = new AutoValue_ToPrettyStringTest_PrimitiveArray(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrimitiveArray{" // force newline
+ + "\n longs = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class PrettyCollection {
+ @Nullable
+ abstract Collection<Object> collection();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void prettyCollection() {
+ PrettyCollection valueType =
+ new AutoValue_ToPrettyStringTest_PrettyCollection(ImmutableList.of("hello", "world"));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyCollection{"
+ + "\n collection = ["
+ + "\n hello,"
+ + "\n world,"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyCollection_elementsWithNewlines() {
+ PrettyCollection valueType =
+ new AutoValue_ToPrettyStringTest_PrettyCollection(
+ ImmutableList.of("hello\nworld\nnewline"));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyCollection{"
+ + "\n collection = ["
+ + "\n hello"
+ + "\n world"
+ + "\n newline,"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyCollection_empty() {
+ PrettyCollection valueType =
+ new AutoValue_ToPrettyStringTest_PrettyCollection(ImmutableList.of());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyCollection{" // force newline
+ + "\n collection = [],"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyCollection_null() {
+ PrettyCollection valueType = new AutoValue_ToPrettyStringTest_PrettyCollection(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyCollection{" // force newline
+ + "\n collection = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class NestedCollection {
+ @Nullable
+ abstract Collection<Collection<Object>> nestedCollection();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void nestedCollection() {
+ NestedCollection valueType =
+ new AutoValue_ToPrettyStringTest_NestedCollection(
+ Arrays.asList(
+ ImmutableList.of("hello", "world"),
+ ImmutableList.of("hello2", "world2"),
+ null,
+ Arrays.asList("not null", null)));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "NestedCollection{"
+ + "\n nestedCollection = ["
+ + "\n ["
+ + "\n hello,"
+ + "\n world,"
+ + "\n ],"
+ + "\n ["
+ + "\n hello2,"
+ + "\n world2,"
+ + "\n ],"
+ + "\n null,"
+ + "\n ["
+ + "\n not null,"
+ + "\n null,"
+ + "\n ],"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @Test
+ public void nestedCollection_elementsWithNewlines() {
+ NestedCollection valueType =
+ new AutoValue_ToPrettyStringTest_NestedCollection(
+ ImmutableList.of(
+ ImmutableList.of((Object) "hello\nworld\nnewline", "hello2\nworld2\nnewline2")));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "NestedCollection{"
+ + "\n nestedCollection = ["
+ + "\n ["
+ + "\n hello"
+ + "\n world"
+ + "\n newline,"
+ + "\n hello2"
+ + "\n world2"
+ + "\n newline2,"
+ + "\n ],"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @Test
+ public void nestedCollection_empty() {
+ NestedCollection valueType =
+ new AutoValue_ToPrettyStringTest_NestedCollection(ImmutableList.of());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "NestedCollection{" // force newline
+ + "\n nestedCollection = [],"
+ + "\n}");
+ }
+
+ @Test
+ public void nestedCollection_nestedEmpty() {
+ NestedCollection valueType =
+ new AutoValue_ToPrettyStringTest_NestedCollection(
+ ImmutableList.of(ImmutableList.of(), ImmutableList.of()));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "NestedCollection{"
+ + "\n nestedCollection = ["
+ + "\n [],"
+ + "\n [],"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @Test
+ public void nestedCollection_null() {
+ NestedCollection valueType = new AutoValue_ToPrettyStringTest_NestedCollection(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "NestedCollection{" // force newline
+ + "\n nestedCollection = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class ImmutablePrimitiveArray {
+ @Nullable
+ abstract ImmutableIntArray immutableIntArray();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void immutablePrimitiveArray() {
+ ImmutablePrimitiveArray valueType =
+ new AutoValue_ToPrettyStringTest_ImmutablePrimitiveArray(ImmutableIntArray.of(1, 2));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "ImmutablePrimitiveArray{"
+ + "\n immutableIntArray = ["
+ + "\n 1,"
+ + "\n 2,"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @Test
+ public void immutablePrimitiveArray_empty() {
+ ImmutablePrimitiveArray valueType =
+ new AutoValue_ToPrettyStringTest_ImmutablePrimitiveArray(ImmutableIntArray.of());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "ImmutablePrimitiveArray{" // force newline
+ + "\n immutableIntArray = [],"
+ + "\n}");
+ }
+
+ @Test
+ public void immutablePrimitiveArray_null() {
+ ImmutablePrimitiveArray valueType =
+ new AutoValue_ToPrettyStringTest_ImmutablePrimitiveArray(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "ImmutablePrimitiveArray{" // force newline
+ + "\n immutableIntArray = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class PrettyMap {
+ @Nullable
+ abstract Map<Object, Object> map();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void prettyMap() {
+ PrettyMap valueType = new AutoValue_ToPrettyStringTest_PrettyMap(ImmutableMap.of(1, 2));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMap{" // force newline
+ + "\n map = {"
+ + "\n 1: 2,"
+ + "\n },"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyMap_keysAndValuesWithNewlines() {
+ PrettyMap valueType =
+ new AutoValue_ToPrettyStringTest_PrettyMap(
+ ImmutableMap.of(
+ "key1\nnewline", "value1\nnewline", "key2\nnewline", "value2\nnewline"));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMap{"
+ + "\n map = {"
+ + "\n key1"
+ + "\n newline: value1"
+ + "\n newline,"
+ + "\n key2"
+ + "\n newline: value2"
+ + "\n newline,"
+ + "\n },"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyMap_empty() {
+ PrettyMap valueType = new AutoValue_ToPrettyStringTest_PrettyMap(ImmutableMap.of());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMap{" // force newline
+ + "\n map = {},"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyMap_null() {
+ PrettyMap valueType = new AutoValue_ToPrettyStringTest_PrettyMap(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMap{" // force newline
+ + "\n map = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class MapOfMaps {
+ @Nullable
+ abstract Map<Map<Object, Object>, Map<Object, Object>> mapOfMaps();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ private static <K, V> Map<K, V> mapWithNulls(K k, V v) {
+ Map<K, V> map = new LinkedHashMap<>();
+ map.put(k, v);
+ return map;
+ }
+
+ @Test
+ public void mapOfMaps() {
+ Map<Map<Object, Object>, Map<Object, Object>> mapOfMaps = new LinkedHashMap<>();
+ mapOfMaps.put(ImmutableMap.of("k1_k", "k1_v"), ImmutableMap.of("v1_k", "v1_v"));
+ mapOfMaps.put(ImmutableMap.of("k2_k", "k2_v"), ImmutableMap.of("v2_k", "v2_v"));
+ mapOfMaps.put(mapWithNulls("keyForNullValue", null), mapWithNulls(null, "valueForNullKey"));
+ mapOfMaps.put(null, ImmutableMap.of("nullKeyKey", "nullKeyValue"));
+ mapOfMaps.put(ImmutableMap.of("nullValueKey", "nullValueValue"), null);
+ mapOfMaps.put(
+ ImmutableMap.of("keyForMapOfNullsKey", "keyForMapOfNullsValue"), mapWithNulls(null, null));
+ MapOfMaps valueType = new AutoValue_ToPrettyStringTest_MapOfMaps(mapOfMaps);
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "MapOfMaps{"
+ + "\n mapOfMaps = {"
+ + "\n {"
+ + "\n k1_k: k1_v,"
+ + "\n }: {"
+ + "\n v1_k: v1_v,"
+ + "\n },"
+ + "\n {"
+ + "\n k2_k: k2_v,"
+ + "\n }: {"
+ + "\n v2_k: v2_v,"
+ + "\n },"
+ + "\n {"
+ + "\n keyForNullValue: null,"
+ + "\n }: {"
+ + "\n null: valueForNullKey,"
+ + "\n },"
+ + "\n null: {"
+ + "\n nullKeyKey: nullKeyValue,"
+ + "\n },"
+ + "\n {"
+ + "\n nullValueKey: nullValueValue,"
+ + "\n }: null,"
+ + "\n {"
+ + "\n keyForMapOfNullsKey: keyForMapOfNullsValue,"
+ + "\n }: {"
+ + "\n null: null,"
+ + "\n },"
+ + "\n },"
+ + "\n}");
+ }
+
+ @Test
+ public void mapOfMaps_elementsWithNewlines() {
+ MapOfMaps valueType =
+ new AutoValue_ToPrettyStringTest_MapOfMaps(
+ ImmutableMap.of(
+ ImmutableMap.of((Object) "k_k\nnewline", (Object) "k_v\nnewline"),
+ ImmutableMap.of((Object) "v_k\nnewline", (Object) "v_v\nnewline")));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "MapOfMaps{"
+ + "\n mapOfMaps = {"
+ + "\n {"
+ + "\n k_k"
+ + "\n newline: k_v"
+ + "\n newline,"
+ + "\n }: {"
+ + "\n v_k"
+ + "\n newline: v_v"
+ + "\n newline,"
+ + "\n },"
+ + "\n },"
+ + "\n}");
+ }
+
+ @Test
+ public void mapOfMaps_empty() {
+ MapOfMaps valueType = new AutoValue_ToPrettyStringTest_MapOfMaps(ImmutableMap.of());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "MapOfMaps{" // force newline
+ + "\n mapOfMaps = {},"
+ + "\n}");
+ }
+
+ @Test
+ public void mapOfMaps_nestedEmpty() {
+ MapOfMaps valueType =
+ new AutoValue_ToPrettyStringTest_MapOfMaps(
+ ImmutableMap.of(ImmutableMap.of(), ImmutableMap.of()));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "MapOfMaps{" // force newline
+ + "\n mapOfMaps = {"
+ + "\n {}: {},"
+ + "\n },"
+ + "\n}");
+ }
+
+ @Test
+ public void mapOfMaps_null() {
+ MapOfMaps valueType = new AutoValue_ToPrettyStringTest_MapOfMaps(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "MapOfMaps{" // force newline
+ + "\n mapOfMaps = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class PrettyMultimap {
+ @Nullable
+ abstract Multimap<Object, Object> multimap();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void prettyMultimap() {
+ PrettyMultimap valueType =
+ new AutoValue_ToPrettyStringTest_PrettyMultimap(
+ ImmutableMultimap.builder().putAll("k", "v1", "v2").build());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMultimap{" // force newline
+ + "\n multimap = {"
+ + "\n k: ["
+ + "\n v1,"
+ + "\n v2,"
+ + "\n ],"
+ + "\n },"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyMultimap_keysAndValuesWithNewlines() {
+ PrettyMultimap valueType =
+ new AutoValue_ToPrettyStringTest_PrettyMultimap(
+ ImmutableMultimap.builder()
+ .putAll("key\nnewline", "value1\nnewline", "value2\nnewline")
+ .build());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMultimap{"
+ + "\n multimap = {"
+ + "\n key"
+ + "\n newline: ["
+ + "\n value1"
+ + "\n newline,"
+ + "\n value2"
+ + "\n newline,"
+ + "\n ],"
+ + "\n },"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyMultimap_empty() {
+ PrettyMultimap valueType =
+ new AutoValue_ToPrettyStringTest_PrettyMultimap(ImmutableMultimap.of());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMultimap{" // force newline
+ + "\n multimap = {},"
+ + "\n}");
+ }
+
+ @Test
+ public void prettyMultimap_null() {
+ PrettyMultimap valueType = new AutoValue_ToPrettyStringTest_PrettyMultimap(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PrettyMultimap{" // force newline
+ + "\n multimap = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class JavaOptional {
+ @Nullable
+ abstract java.util.Optional<Object> optional();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void javaOptional_present() {
+ JavaOptional valueType =
+ new AutoValue_ToPrettyStringTest_JavaOptional(java.util.Optional.of("hello, world"));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "JavaOptional{" // force newline
+ + "\n optional = hello, world,"
+ + "\n}");
+ }
+
+ @Test
+ public void javaOptional_empty() {
+ JavaOptional valueType =
+ new AutoValue_ToPrettyStringTest_JavaOptional(java.util.Optional.empty());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "JavaOptional{" // force newline
+ + "\n optional = <empty>,"
+ + "\n}");
+ }
+
+ @Test
+ public void javaOptional_valueWithNewlines() {
+ JavaOptional valueType =
+ new AutoValue_ToPrettyStringTest_JavaOptional(
+ java.util.Optional.of("optional\nwith\nnewline"));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "JavaOptional{" // force newline
+ + "\n optional = optional"
+ + "\n with"
+ + "\n newline,"
+ + "\n}");
+ }
+
+ @Test
+ public void javaOptional_null() {
+ @SuppressWarnings("NullOptional")
+ JavaOptional valueType = new AutoValue_ToPrettyStringTest_JavaOptional(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "JavaOptional{" // force newline
+ + "\n optional = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class GuavaOptional {
+ @Nullable
+ abstract com.google.common.base.Optional<Object> optional();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void guavaOptional_present() {
+ GuavaOptional valueType =
+ new AutoValue_ToPrettyStringTest_GuavaOptional(
+ com.google.common.base.Optional.of("hello, world"));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "GuavaOptional{" // force newline
+ + "\n optional = hello, world,"
+ + "\n}");
+ }
+
+ @Test
+ public void guavaOptional_absent() {
+ GuavaOptional valueType =
+ new AutoValue_ToPrettyStringTest_GuavaOptional(com.google.common.base.Optional.absent());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "GuavaOptional{" // force newline
+ + "\n optional = <absent>,"
+ + "\n}");
+ }
+
+ @Test
+ public void guavaOptional_valueWithNewlines() {
+ GuavaOptional valueType =
+ new AutoValue_ToPrettyStringTest_GuavaOptional(
+ com.google.common.base.Optional.of("optional\nwith\nnewline"));
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "GuavaOptional{" // force newline
+ + "\n optional = optional"
+ + "\n with"
+ + "\n newline,"
+ + "\n}");
+ }
+
+ @Test
+ public void guavaOptional_null() {
+ @SuppressWarnings("NullOptional")
+ GuavaOptional valueType = new AutoValue_ToPrettyStringTest_GuavaOptional(null);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "GuavaOptional{" // force newline
+ + "\n optional = null,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class NestAllTheThings {
+ @Nullable
+ abstract com.google.common.base.Optional<
+ java.util.Optional<
+ List< // open list
+ Map<ImmutableIntArray, Multimap<int[][], Object>>
+ // close list
+ >>>
+ value();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void nestAllTheThings() {
+ NestAllTheThings valueType =
+ new AutoValue_ToPrettyStringTest_NestAllTheThings(
+ com.google.common.base.Optional.of(
+ java.util.Optional.of(
+ ImmutableList.of(
+ ImmutableMap.of(
+ ImmutableIntArray.of(-1, -2, -3),
+ ImmutableMultimap.of(
+ new int[][] {{1, 2}, {3, 4, 5}, {}}, "value\nwith\nnewline"))))));
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "NestAllTheThings{"
+ + "\n value = ["
+ + "\n {"
+ + "\n ["
+ + "\n -1,"
+ + "\n -2,"
+ + "\n -3,"
+ + "\n ]: {"
+ + "\n ["
+ + "\n ["
+ + "\n 1,"
+ + "\n 2,"
+ + "\n ],"
+ + "\n ["
+ + "\n 3,"
+ + "\n 4,"
+ + "\n 5,"
+ + "\n ],"
+ + "\n [],"
+ + "\n ]: ["
+ + "\n value"
+ + "\n with"
+ + "\n newline,"
+ + "\n ],"
+ + "\n },"
+ + "\n },"
+ + "\n ],"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class WithCustomName {
+ abstract int i();
+
+ @ToPrettyString
+ abstract String customName();
+ }
+
+ @Test
+ public void withCustomName() {
+ WithCustomName valueType = new AutoValue_ToPrettyStringTest_WithCustomName(1);
+
+ assertThat(valueType.customName())
+ .isEqualTo(
+ "WithCustomName{" // force newline
+ + "\n i = 1,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class OverridesToString {
+ abstract int i();
+
+ @ToPrettyString
+ @Override
+ public abstract String toString();
+ }
+
+ @Test
+ public void overridesToString() {
+ OverridesToString valueType = new AutoValue_ToPrettyStringTest_OverridesToString(1);
+
+ assertThat(valueType.toString())
+ .isEqualTo(
+ "OverridesToString{" // force newline
+ + "\n i = 1,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class PropertyHasToPrettyString {
+ static class HasToPrettyString<A> {
+ @Override
+ public String toString() {
+ throw new AssertionError();
+ }
+
+ @ToPrettyString
+ String toPrettyString() {
+ return "custom\n@ToPrettyString\nmethod";
+ }
+ }
+
+ static class HasInheritedToPrettyString extends HasToPrettyString<String> {}
+
+ interface HasToPrettyStringInInterface {
+ @ToPrettyString
+ default String toPrettyString() {
+ return "custom\n@ToPrettyString\nmethod\ninterface";
+ }
+ }
+
+ static class HasToPrettyStringFromSuperInterface implements HasToPrettyStringInInterface {}
+
+ abstract HasToPrettyString<String> parameterizedWithString();
+
+ abstract HasToPrettyString<Void> parameterizedWithVoid();
+
+ abstract HasInheritedToPrettyString superclass();
+
+ abstract HasToPrettyStringFromSuperInterface superinterface();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void propertyHasToPrettyString() {
+ PropertyHasToPrettyString valueType =
+ new AutoValue_ToPrettyStringTest_PropertyHasToPrettyString(
+ new PropertyHasToPrettyString.HasToPrettyString<>(),
+ new PropertyHasToPrettyString.HasToPrettyString<>(),
+ new PropertyHasToPrettyString.HasInheritedToPrettyString(),
+ new PropertyHasToPrettyString.HasToPrettyStringFromSuperInterface());
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "PropertyHasToPrettyString{"
+ + "\n parameterizedWithString = custom"
+ + "\n @ToPrettyString"
+ + "\n method,"
+ + "\n parameterizedWithVoid = custom"
+ + "\n @ToPrettyString"
+ + "\n method,"
+ + "\n superclass = custom"
+ + "\n @ToPrettyString"
+ + "\n method,"
+ + "\n superinterface = custom"
+ + "\n @ToPrettyString"
+ + "\n method"
+ + "\n interface,"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class CollectionSubtypesWithFixedTypeParameters {
+ static class StringList extends ArrayList<String> {}
+
+ static class StringMap extends LinkedHashMap<String, String> {}
+
+ abstract StringList list();
+
+ abstract StringMap map();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void fixedTypeParameters() {
+ StringList stringList = new StringList();
+ stringList.addAll(ImmutableList.of("a", "b", "c"));
+ StringMap stringMap = new StringMap();
+ stringMap.putAll(ImmutableMap.of("A", "a", "B", "b"));
+ CollectionSubtypesWithFixedTypeParameters valueType =
+ new AutoValue_ToPrettyStringTest_CollectionSubtypesWithFixedTypeParameters(
+ stringList, stringMap);
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "CollectionSubtypesWithFixedTypeParameters{"
+ + "\n list = ["
+ + "\n a,"
+ + "\n b,"
+ + "\n c,"
+ + "\n ],"
+ + "\n map = {"
+ + "\n A: a,"
+ + "\n B: b,"
+ + "\n },"
+ + "\n}");
+ }
+
+ @AutoValue
+ abstract static class JavaBeans {
+ abstract int getInt();
+
+ abstract boolean isBoolean();
+
+ abstract String getNotAJavaIdentifier();
+
+ @ToPrettyString
+ abstract String toPrettyString();
+ }
+
+ @Test
+ public void javaBeans() {
+ JavaBeans valueType = new AutoValue_ToPrettyStringTest_JavaBeans(4, false, "not");
+
+ assertThat(valueType.toPrettyString())
+ .isEqualTo(
+ "JavaBeans{"
+ + "\n int = 4,"
+ + "\n boolean = false,"
+ + "\n notAJavaIdentifier = not,"
+ + "\n}");
+
+ // Check to make sure that we use the same property names that AutoValue does. This is mostly
+ // defensive, since in some scenarios AutoValue considers the property names of a java bean as
+ // having the prefix removed.
+ assertThat(valueType.toString())
+ .isEqualTo("JavaBeans{int=4, boolean=false, notAJavaIdentifier=not}");
+ }
+}
diff --git a/value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringValidatorTest.java b/value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringValidatorTest.java
new file mode 100644
index 00000000..6c51be1d
--- /dev/null
+++ b/value/src/test/java/com/google/auto/value/extension/toprettystring/ToPrettyStringValidatorTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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 com.google.testing.compile.CompilationSubject.assertThat;
+import static com.google.testing.compile.Compiler.javac;
+
+import com.google.auto.value.extension.toprettystring.processor.ToPrettyStringValidator;
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.JavaFileObjects;
+import javax.tools.JavaFileObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ToPrettyStringValidatorTest {
+ @Test
+ public void cannotBeStatic() {
+ JavaFileObject file =
+ JavaFileObjects.forSourceLines(
+ "test.Test",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "class Test {",
+ " @ToPrettyString",
+ " static String toPretty() {",
+ " return new String();",
+ " }",
+ "}",
+ "");
+ Compilation compilation = compile(file);
+
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorCount(1);
+ assertThat(compilation)
+ .hadErrorContaining("must be instance methods")
+ .inFile(file)
+ .onLineContaining("static String toPretty()");
+ }
+
+ @Test
+ public void mustReturnString() {
+ JavaFileObject file =
+ JavaFileObjects.forSourceLines(
+ "test.Test",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "class Test {",
+ " @ToPrettyString",
+ " CharSequence toPretty() {",
+ " return new String();",
+ " }",
+ "}",
+ "");
+ Compilation compilation = compile(file);
+
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorCount(1);
+ assertThat(compilation)
+ .hadErrorContaining("must return String")
+ .inFile(file)
+ .onLineContaining("CharSequence toPretty()");
+ }
+
+ @Test
+ public void noParameters() {
+ JavaFileObject file =
+ JavaFileObjects.forSourceLines(
+ "test.Test",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "class Test {",
+ " @ToPrettyString",
+ " String toPretty(String value) {",
+ " return value;",
+ " }",
+ "}",
+ "");
+ Compilation compilation = compile(file);
+
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorCount(1);
+ assertThat(compilation)
+ .hadErrorContaining("cannot have parameters")
+ .inFile(file)
+ .onLineContaining("String toPretty(String value)");
+ }
+
+ @Test
+ public void onlyOneToPrettyStringMethod_sameClass() {
+ JavaFileObject file =
+ JavaFileObjects.forSourceLines(
+ "test.Test",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "class Test {",
+ " @ToPrettyString",
+ " String toPretty1() {",
+ " return new String();",
+ " }",
+ "",
+ " @ToPrettyString",
+ " String toPretty2() {",
+ " return new String();",
+ " }",
+ "}",
+ "");
+ Compilation compilation = compile(file);
+
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorCount(1);
+ assertThat(compilation)
+ .hadErrorContaining(
+ error(
+ "test.Test has multiple @ToPrettyString methods:",
+ " - test.Test.toPretty1()",
+ " - test.Test.toPretty2()"))
+ .inFile(file)
+ .onLineContaining("class Test");
+ }
+
+ @Test
+ public void onlyOneToPrettyStringMethod_superclass() {
+ JavaFileObject superclass =
+ JavaFileObjects.forSourceLines(
+ "test.Superclass",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "class Superclass {",
+ " @ToPrettyString",
+ " String toPretty1() {",
+ " return new String();",
+ " }",
+ "}",
+ "");
+ JavaFileObject subclass =
+ JavaFileObjects.forSourceLines(
+ "test.Subclass",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "class Subclass extends Superclass {",
+ " @ToPrettyString",
+ " String toPretty2() {",
+ " return new String();",
+ " }",
+ "}",
+ "");
+ Compilation compilation = compile(superclass, subclass);
+
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorCount(1);
+ assertThat(compilation)
+ .hadErrorContaining(
+ error(
+ "test.Subclass has multiple @ToPrettyString methods:",
+ " - test.Superclass.toPretty1()",
+ " - test.Subclass.toPretty2()"))
+ .inFile(subclass)
+ .onLineContaining("class Subclass");
+ }
+
+ @Test
+ public void onlyOneToPrettyStringMethod_superinterface() {
+ JavaFileObject superinterface =
+ JavaFileObjects.forSourceLines(
+ "test.Superinterface",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "interface Superinterface {",
+ " @ToPrettyString",
+ " default String toPretty1() {",
+ " return new String();",
+ " }",
+ "}",
+ "");
+ JavaFileObject subclass =
+ JavaFileObjects.forSourceLines(
+ "test.Subclass",
+ "package test;",
+ "",
+ "import com.google.auto.value.extension.toprettystring.ToPrettyString;",
+ "",
+ "class Subclass implements Superinterface {",
+ " @ToPrettyString",
+ " String toPretty2() {",
+ " return new String();",
+ " }",
+ "}",
+ "");
+ Compilation compilation = compile(superinterface, subclass);
+
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorCount(1);
+ assertThat(compilation)
+ .hadErrorContaining(
+ error(
+ "test.Subclass has multiple @ToPrettyString methods:",
+ " - test.Superinterface.toPretty1()",
+ " - test.Subclass.toPretty2()"))
+ .inFile(subclass)
+ .onLineContaining("class Subclass");
+ }
+
+ private static Compilation compile(JavaFileObject... javaFileObjects) {
+ return javac().withProcessors(new ToPrettyStringValidator()).compile(javaFileObjects);
+ }
+
+ private static String error(String... lines) {
+ return String.join("\n ", lines);
+ }
+}
diff --git a/value/src/test/java/com/google/auto/value/processor/IncrementalExtensionTest.java b/value/src/test/java/com/google/auto/value/processor/IncrementalExtensionTest.java
index 27cb0936..4f526edb 100644
--- a/value/src/test/java/com/google/auto/value/processor/IncrementalExtensionTest.java
+++ b/value/src/test/java/com/google/auto/value/processor/IncrementalExtensionTest.java
@@ -22,6 +22,7 @@ import com.google.auto.value.extension.AutoValueExtension;
import com.google.auto.value.extension.AutoValueExtension.IncrementalExtensionType;
import com.google.auto.value.extension.memoized.processor.MemoizeExtension;
import com.google.auto.value.extension.serializable.processor.SerializableAutoValueExtension;
+import com.google.auto.value.extension.toprettystring.processor.ToPrettyStringExtension;
import com.google.common.collect.ImmutableList;
import javax.annotation.processing.ProcessingEnvironment;
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType;
@@ -46,7 +47,10 @@ public class IncrementalExtensionTest {
// different <?>.
assertThat(builtInExtensions)
.comparingElementsUsing(transforming(e -> (Object) e.getClass(), "is class"))
- .containsExactly(MemoizeExtension.class, SerializableAutoValueExtension.class);
+ .containsExactly(
+ MemoizeExtension.class,
+ SerializableAutoValueExtension.class,
+ ToPrettyStringExtension.class);
AutoValueProcessor processor = new AutoValueProcessor(builtInExtensions);
assertThat(processor.getSupportedOptions())