diff options
Diffstat (limited to 'value/src')
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()) |