com.google.auto.value.extension.toprettystring.processor.ToPrettyStringExtension Maven / Gradle / Ivy
Show all versions of auto-value Show documentation
/*
* 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.MoreStreams.toImmutableList;
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.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 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 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.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, MethodSpec> delegateMethods =
new LinkedHashMap<>();
private final Set 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.
*
* 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 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 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 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 methodBody) {
Equivalence.Wrapper 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 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 allAppendedValues =
ImmutableList.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) ? "" : "")
.build();
}
private ImmutableList 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() {
@Override
public String visitDeclared(DeclaredType type, Void v) {
String simpleName = simpleNameForType(type);
if (type.getTypeArguments().isEmpty()) {
return simpleName;
}
ImmutableList 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 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() {
@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 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 KINDS_BY_SUPERTYPE =
ImmutableMap.of(
"java.util.Collection", COLLECTION,
"java.util.Map", MAP,
"com.google.common.collect.Multimap", MULTIMAP);
static class KindVisitor extends SimpleTypeVisitor8 {
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 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 consumeMethods(Context context) {
return toPrettyStringMethods(context);
}
@Override
public IncrementalExtensionType incrementalType(ProcessingEnvironment processingEnvironment) {
return IncrementalExtensionType.ISOLATING;
}
}