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