com.google.auto.value.processor.BuilderMethodClassifier.java Source code

Java tutorial

Introduction

Here is the source code for com.google.auto.value.processor.BuilderMethodClassifier.java

Source

/*
 * Copyright (C) 2015 Google, Inc.
 *
 * 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.processor;

import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.value.processor.PropertyBuilderClassifier.PropertyBuilder;
import com.google.common.base.Equivalence;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import java.beans.Introspector;
import java.util.Map;
import java.util.Set;
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.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

/**
 * Classifies methods inside builder types, based on their names and parameter and return types.
 *
 * @author amonn McManus
 */
class BuilderMethodClassifier {
    private static final Equivalence<TypeMirror> TYPE_EQUIVALENCE = MoreTypes.equivalence();

    private final ErrorReporter errorReporter;
    private final Types typeUtils;
    private final Elements elementUtils;
    private final TypeElement autoValueClass;
    private final TypeElement builderType;
    private final ImmutableBiMap<ExecutableElement, String> getterToPropertyName;
    private final ImmutableMap<String, ExecutableElement> getterNameToGetter;
    private final TypeSimplifier typeSimplifier;

    private final Set<ExecutableElement> buildMethods = Sets.newLinkedHashSet();
    private final Map<String, BuilderSpec.PropertyGetter> builderGetters = Maps.newLinkedHashMap();
    private final Multimap<String, ExecutableElement> propertyNameToPrefixedSetters = LinkedListMultimap.create();
    private final Multimap<String, ExecutableElement> propertyNameToUnprefixedSetters = LinkedListMultimap.create();
    private final Map<String, PropertyBuilder> propertyNameToPropertyBuilder = Maps.newLinkedHashMap();
    private final EclipseHack eclipseHack;

    private boolean settersPrefixed;

    private BuilderMethodClassifier(ErrorReporter errorReporter, ProcessingEnvironment processingEnv,
            TypeElement autoValueClass, TypeElement builderType,
            ImmutableBiMap<ExecutableElement, String> getterToPropertyName, TypeSimplifier typeSimplifier) {
        this.errorReporter = errorReporter;
        this.typeUtils = processingEnv.getTypeUtils();
        this.elementUtils = processingEnv.getElementUtils();
        this.autoValueClass = autoValueClass;
        this.builderType = builderType;
        this.getterToPropertyName = getterToPropertyName;
        ImmutableMap.Builder<String, ExecutableElement> getterToPropertyNameBuilder = ImmutableMap.builder();
        for (ExecutableElement getter : getterToPropertyName.keySet()) {
            getterToPropertyNameBuilder.put(getter.getSimpleName().toString(), getter);
        }
        this.getterNameToGetter = getterToPropertyNameBuilder.build();
        this.typeSimplifier = typeSimplifier;
        this.eclipseHack = new EclipseHack(processingEnv);
    }

    /**
     * Classifies the given methods from a builder type and its ancestors.
     *
     * @param methods the methods in {@code builderType} and its ancestors.
     * @param errorReporter where to report errors.
     * @param processingEnv the ProcessingEnvironment for annotation processing.
     * @param autoValueClass the {@code AutoValue} class containing the builder.
     * @param builderType the builder class or interface within {@code autoValueClass}.
     * @param getterToPropertyName a map from getter methods to the properties they get.
     * @param typeSimplifier the TypeSimplifier that will be used to control imports.
     * @param autoValueHasToBuilder true if the containing {@code @AutoValue} class has a
     *     {@code toBuilder()} method.
     *
     * @return an {@code Optional} that contains the results of the classification if it was
     *     successful or nothing if it was not.
     */
    static Optional<BuilderMethodClassifier> classify(Iterable<ExecutableElement> methods,
            ErrorReporter errorReporter, ProcessingEnvironment processingEnv, TypeElement autoValueClass,
            TypeElement builderType, ImmutableBiMap<ExecutableElement, String> getterToPropertyName,
            TypeSimplifier typeSimplifier, boolean autoValueHasToBuilder) {
        BuilderMethodClassifier classifier = new BuilderMethodClassifier(errorReporter, processingEnv,
                autoValueClass, builderType, getterToPropertyName, typeSimplifier);
        if (classifier.classifyMethods(methods, autoValueHasToBuilder)) {
            return Optional.of(classifier);
        } else {
            return Optional.absent();
        }
    }

    /**
     * Returns a multimap from the name of a property to the methods that set it. If the property is
     * defined by an abstract method in the {@code @AutoValue} class called {@code foo()} or
     * {@code getFoo()} then the name of the property is {@code foo} and there will be an entry in
     * the map where the key is {@code "foo"} and the value is a method in the builder called
     * {@code foo} or {@code setFoo}.
     */
    ImmutableMultimap<String, ExecutableElement> propertyNameToSetters() {
        return ImmutableMultimap
                .copyOf(settersPrefixed ? propertyNameToPrefixedSetters : propertyNameToUnprefixedSetters);
    }

    Map<String, PropertyBuilder> propertyNameToPropertyBuilder() {
        return propertyNameToPropertyBuilder;
    }

    /**
     * Returns the set of properties that have getters in the builder. If a property is defined by
     * an abstract method in the {@code @AutoValue} class called {@code foo()} or {@code getFoo()}
     * then the name of the property is {@code foo}, If the builder also has a method of the same name
     * ({@code foo()} or {@code getFoo()}) then the set returned here will contain {@code foo}.
     */
    ImmutableMap<String, BuilderSpec.PropertyGetter> builderGetters() {
        return ImmutableMap.copyOf(builderGetters);
    }

    /**
     * Returns the methods that were identified as {@code build()} methods. These are methods that
     * have no parameters and return the {@code @AutoValue} type, conventionally called
     * {@code build()}.
     */
    Set<ExecutableElement> buildMethods() {
        return ImmutableSet.copyOf(buildMethods);
    }

    /**
     * Classifies the given methods and sets the state of this object based on what is found.
     */
    private boolean classifyMethods(Iterable<ExecutableElement> methods, boolean autoValueHasToBuilder) {
        boolean ok = true;
        for (ExecutableElement method : methods) {
            ok &= classifyMethod(method);
        }
        if (!ok) {
            return false;
        }
        Multimap<String, ExecutableElement> propertyNameToSetter;
        if (propertyNameToPrefixedSetters.isEmpty()) {
            propertyNameToSetter = propertyNameToUnprefixedSetters;
            this.settersPrefixed = false;
        } else if (propertyNameToUnprefixedSetters.isEmpty()) {
            propertyNameToSetter = propertyNameToPrefixedSetters;
            this.settersPrefixed = true;
        } else {
            errorReporter.reportError("If any setter methods use the setFoo convention then all must",
                    propertyNameToUnprefixedSetters.values().iterator().next());
            return false;
        }
        for (Map.Entry<ExecutableElement, String> getterEntry : getterToPropertyName.entrySet()) {
            String property = getterEntry.getValue();
            String propertyType = typeSimplifier.simplify(getterEntry.getKey().getReturnType());
            boolean hasSetter = propertyNameToSetter.containsKey(property);
            PropertyBuilder propertyBuilder = propertyNameToPropertyBuilder.get(property);
            boolean hasBuilder = propertyBuilder != null;
            if (hasBuilder) {
                // If property bar of type Bar has a barBuilder() that returns BarBuilder, then it must be
                // possible to make a BarBuilder from a Bar if either (1) the @AutoValue class has a
                // toBuilder() or (2) there is also a setBar(Bar). Making BarBuilder from Bar is possible
                // if Bar either has a toBuilder() method or is a Guava immutable collection (in which case
                // we can use addAll or putAll).
                boolean canMakeBarBuilder = (propertyBuilder.getBuiltToBuilder() != null
                        || propertyBuilder.getCopyAll() != null);
                boolean needToMakeBarBuilder = (autoValueHasToBuilder || hasSetter);
                if (needToMakeBarBuilder && !canMakeBarBuilder) {
                    String error = String.format(
                            "Property builder method returns %1$s but there is no way to make that type from "
                                    + "%2$s: %2$s does not have a non-static toBuilder() method that returns %1$s",
                            propertyBuilder.getBuilderType(), propertyType);
                    errorReporter.reportError(error, propertyBuilder.getPropertyBuilderMethod());
                }
            } else if (!hasSetter) {
                // We have neither barBuilder() nor setBar(Bar), so we should complain.
                String setterName = settersPrefixed ? prefixWithSet(property) : property;
                String error = String.format(
                        "Expected a method with this signature: %s%s %s(%s), or a %sBuilder() method", builderType,
                        typeParamsString(), setterName, propertyType, property);
                errorReporter.reportError(error, builderType);
                ok = false;
            }
        }
        return ok;
    }

    /**
     * Classifies a method and update the state of this object based on what is found.
     *
     * @return true if the method was successfully classified, false if an error has been reported.
     */
    private boolean classifyMethod(ExecutableElement method) {
        switch (method.getParameters().size()) {
        case 0:
            return classifyMethodNoArgs(method);
        case 1:
            return classifyMethodOneArg(method);
        default:
            errorReporter.reportError("Builder methods must have 0 or 1 parameters", method);
            return false;
        }
    }

    /**
     * Classifies a method given that it has no arguments. Currently a method with no
     * arguments can be a {@code build()} method, meaning that its return type must be the
     * {@code @AutoValue} class; it can be a getter, with the same signature as one of
     * the property getters in the {@code @AutoValue} class; or it can be a property builder,
     * like {@code ImmutableList.Builder<String> foosBuilder()} for the property defined by
     * {@code ImmutableList<String> foos()} or {@code getFoos()}.
     *
     * @return true if the method was successfully classified, false if an error has been reported.
     */
    private boolean classifyMethodNoArgs(ExecutableElement method) {
        String methodName = method.getSimpleName().toString();
        TypeMirror returnType = builderMethodReturnType(method);

        ExecutableElement getter = getterNameToGetter.get(methodName);
        if (getter != null) {
            return classifyGetter(method, getter);
        }

        if (methodName.endsWith("Builder")) {
            String property = methodName.substring(0, methodName.length() - "Builder".length());
            if (getterToPropertyName.containsValue(property)) {
                PropertyBuilderClassifier propertyBuilderClassifier = new PropertyBuilderClassifier(errorReporter,
                        typeUtils, elementUtils, this, getterToPropertyName, typeSimplifier, eclipseHack);
                Optional<PropertyBuilder> propertyBuilder = propertyBuilderClassifier.makePropertyBuilder(method,
                        property);
                if (propertyBuilder.isPresent()) {
                    propertyNameToPropertyBuilder.put(property, propertyBuilder.get());
                    return true;
                } else {
                    return false;
                }
            }
        }

        if (TYPE_EQUIVALENCE.equivalent(returnType, autoValueClass.asType())) {
            buildMethods.add(method);
            return true;
        }

        String error = String.format(
                "Method without arguments should be a build method returning %1$s%2$s"
                        + " or a getter method with the same name and type as a getter method of %1$s",
                autoValueClass, typeParamsString());
        errorReporter.reportError(error, method);
        return false;
    }

    private boolean classifyGetter(ExecutableElement builderGetter, ExecutableElement originalGetter) {
        String propertyName = getterToPropertyName.get(originalGetter);
        TypeMirror builderGetterType = builderMethodReturnType(builderGetter);
        String builderGetterTypeString = typeSimplifier.simplify(builderGetterType);
        TypeMirror originalGetterType = originalGetter.getReturnType();
        if (TYPE_EQUIVALENCE.equivalent(builderGetterType, originalGetterType)) {
            builderGetters.put(propertyName,
                    new BuilderSpec.PropertyGetter(builderGetter, builderGetterTypeString, null));
            return true;
        }
        Optionalish optional = Optionalish.createIfOptional(builderGetterType,
                typeSimplifier.simplifyRaw(builderGetterType));
        if (optional != null) {
            TypeMirror containedType = optional.getContainedType(typeUtils);
            // If the original method is int getFoo() then we allow Optional<Integer> here.
            // boxedOriginalType is Integer, and containedType is also Integer.
            // We don't need any special code for OptionalInt because containedType will be int then.
            TypeMirror boxedOriginalType = (originalGetterType.getKind().isPrimitive())
                    ? typeUtils.boxedClass(MoreTypes.asPrimitiveType(originalGetterType)).asType()
                    : null;
            if (TYPE_EQUIVALENCE.equivalent(containedType, originalGetterType)
                    || TYPE_EQUIVALENCE.equivalent(containedType, boxedOriginalType)) {
                builderGetters.put(propertyName,
                        new BuilderSpec.PropertyGetter(builderGetter, builderGetterTypeString, optional));
                return true;
            }
        }
        String error = String.format(
                "Method matches a property of %1$s but has return type %2$s instead of %3$s "
                        + "or an Optional wrapping of %3$s",
                autoValueClass, builderGetterType, originalGetter.getReturnType());
        errorReporter.reportError(error, builderGetter);
        return false;
    }

    /**
     * Classifies a method given that it has one argument. Currently, a method with one argument can
     * only be a setter, meaning that it must look like {@code foo(T)} or {@code setFoo(T)}, where
     * the {@code AutoValue} class has a property called {@code foo} of type {@code T}.
     *
     * @return true if the method was successfully classified, false if an error has been reported.
     */
    private boolean classifyMethodOneArg(ExecutableElement method) {
        String methodName = method.getSimpleName().toString();
        Map<String, ExecutableElement> propertyNameToGetter = getterToPropertyName.inverse();
        String propertyName = null;
        ExecutableElement valueGetter = propertyNameToGetter.get(methodName);
        Multimap<String, ExecutableElement> propertyNameToSetters = null;
        if (valueGetter != null) {
            propertyNameToSetters = propertyNameToUnprefixedSetters;
            propertyName = methodName;
        } else if (valueGetter == null && methodName.startsWith("set") && methodName.length() > 3) {
            propertyNameToSetters = propertyNameToPrefixedSetters;
            propertyName = Introspector.decapitalize(methodName.substring(3));
            valueGetter = propertyNameToGetter.get(propertyName);
            if (valueGetter == null) {
                // If our property is defined by a getter called getOAuth() then it is called "OAuth"
                // because of Introspector.decapitalize. Therefore we want Introspector.decapitalize to
                // be used for the setter too, so that you can write setOAuth(x). Meanwhile if the property
                // is defined by a getter called oAuth() then it is called "oAuth", but you would still
                // expect to be able to set it using setOAuth(x). Hence the second try using a decapitalize
                // method without the quirky two-leading-capitals rule.
                propertyName = decapitalize(methodName.substring(3));
                valueGetter = propertyNameToGetter.get(propertyName);
            }
        }
        if (valueGetter == null || propertyNameToSetters == null) {
            // The second disjunct isn't needed but convinces control-flow checkers that
            // propertyNameToSetters can't be null when we call put on it below.
            errorReporter.reportError("Method does not correspond to a property of " + autoValueClass, method);
            checkForFailedJavaBean(method);
            return false;
        }
        if (!checkSetterParameter(valueGetter, method)) {
            return false;
        } else if (!TYPE_EQUIVALENCE.equivalent(builderMethodReturnType(method), builderType.asType())) {
            errorReporter.reportError("Setter methods must return " + builderType + typeParamsString(), method);
            return false;
        } else {
            propertyNameToSetters.put(propertyName, method);
            return true;
        }
    }

    // A frequence source of problems is where the JavaBeans conventions have been followed for
    // most but not all getters. Then AutoValue considers that they haven't been followed at all,
    // so you might have a property called getFoo where you thought it was called just foo, and
    // you might not understand why your setter called setFoo is rejected (it would have to be called
    // setGetFoo).
    private void checkForFailedJavaBean(ExecutableElement rejectedSetter) {
        ImmutableSet<ExecutableElement> allGetters = getterToPropertyName.keySet();
        ImmutableSet<ExecutableElement> prefixedGetters = AutoValueProcessor.prefixedGettersIn(allGetters);
        if (prefixedGetters.size() < allGetters.size() && prefixedGetters.size() >= allGetters.size() / 2) {
            String note = "This might be because you are using the getFoo() convention"
                    + " for some but not all methods. These methods don't follow the convention: "
                    + Sets.difference(allGetters, prefixedGetters);
            errorReporter.reportNote(note, rejectedSetter);
        }
    }

    /**
     * Checks that the given setter method has a parameter type that is compatible with the return
     * type of the given getter. Compatible means either that it is the same, or that it is a type
     * that can be copied using a method like {@code ImmutableList.copyOf} or {@code Optional.of}.
     *
     * @return true if the types correspond, false if an error has been reported.
     */
    private boolean checkSetterParameter(ExecutableElement valueGetter, ExecutableElement setter) {
        TypeMirror targetType = valueGetter.getReturnType();
        TypeMirror parameterType = setter.getParameters().get(0).asType();
        if (TYPE_EQUIVALENCE.equivalent(parameterType, targetType)) {
            return true;
        }
        ImmutableList<ExecutableElement> copyOfMethods = copyOfMethods(targetType);
        if (!copyOfMethods.isEmpty()) {
            return canMakeCopyUsing(copyOfMethods, valueGetter, setter);
        }
        String error = String.format("Parameter type %s of setter method should be %s to match getter %s.%s",
                parameterType, targetType, autoValueClass, valueGetter.getSimpleName());
        errorReporter.reportError(error, setter);
        return false;
    }

    /**
     * Checks that the given setter method has a parameter type that can be copied to the return type
     * of the given getter using one of the given {@code copyOf} methods.
     *
     * @return true if the copy can be made, false if an error has been reported.
     */
    private boolean canMakeCopyUsing(ImmutableList<ExecutableElement> copyOfMethods, ExecutableElement valueGetter,
            ExecutableElement setter) {
        TypeMirror targetType = valueGetter.getReturnType();
        TypeMirror parameterType = setter.getParameters().get(0).asType();
        for (ExecutableElement copyOfMethod : copyOfMethods) {
            if (canMakeCopyUsing(copyOfMethod, targetType, parameterType)) {
                return true;
            }
        }
        DeclaredType targetDeclaredType = MoreTypes.asDeclared(targetType);
        String targetTypeSimpleName = targetDeclaredType.asElement().getSimpleName().toString();
        String error = String.format(
                "Parameter type of setter method should be %s to match getter %s.%s, or it should be a "
                        + "type that can be passed to %s.copyOf",
                targetType, autoValueClass, valueGetter.getSimpleName(), targetTypeSimpleName);
        errorReporter.reportError(error, setter);
        return false;
    }

    /**
     * Returns true if {@code copyOfMethod} can be used to copy the {@code parameterType}
     * to the {@code targetType}.
     */
    private boolean canMakeCopyUsing(ExecutableElement copyOfMethod, TypeMirror targetType,
            TypeMirror parameterType) {
        // We have a parameter type, for example Set<? extends T>, and we want to know if it can be
        // passed to the given copyOf method, which might for example be one of these methods from
        // ImmutableSet:
        //    public static <E> ImmutableSet<E> copyOf(Collection<? extends E> elements)
        //    public static <E> ImmutableSet<E> copyOf(E[] elements)
        // Additionally, if it can indeed be passed to the method, we want to know whether the result
        // (here ImmutableSet<? extends T>) is compatible with the property to be set, bearing in mind
        // that the T in question is the one from the @AutoValue class and not the Builder class.
        // The logic to do that properly would be quite complex, and we don't get much help from the
        // annotation processing API, so for now we simply check that the erased types correspond.
        // This means that an incorrect type will lead to a compilation error in the generated code,
        // which is less than ideal.
        // TODO(b/20691134): make this work properly
        TypeMirror erasedParameterType = typeUtils.erasure(parameterType);
        TypeMirror erasedCopyOfParameterType = typeUtils
                .erasure(Iterables.getOnlyElement(copyOfMethod.getParameters()).asType());
        // erasedParameterType is Set in the example and erasedCopyOfParameterType is Collection
        if (!typeUtils.isAssignable(erasedParameterType, erasedCopyOfParameterType)) {
            return false;
        }
        TypeMirror erasedCopyOfReturnType = typeUtils.erasure(copyOfMethod.getReturnType());
        TypeMirror erasedTargetType = typeUtils.erasure(targetType);
        // erasedCopyOfReturnType and erasedTargetType are both ImmutableSet in the example.
        // In fact for Guava immutable collections the check could be for equality.
        return typeUtils.isAssignable(erasedCopyOfReturnType, erasedTargetType);
    }

    /**
     * Returns {@code copyOf} methods from the given type. These are static methods called
     * {@code copyOf} with a single parameter. All of Guava's concrete immutable collection types have
     * at least one such method, but we will also accept other classes with an appropriate method,
     * such as {@link java.util.EnumSet}.
     */
    private ImmutableList<ExecutableElement> copyOfMethods(TypeMirror targetType) {
        if (!targetType.getKind().equals(TypeKind.DECLARED)) {
            return ImmutableList.of();
        }
        String copyOf = Optionalish.isOptional(targetType) ? "of" : "copyOf";
        TypeElement immutableTargetType = MoreElements.asType(typeUtils.asElement(targetType));
        ImmutableList.Builder<ExecutableElement> copyOfMethods = ImmutableList.builder();
        for (ExecutableElement method : ElementFilter.methodsIn(immutableTargetType.getEnclosedElements())) {
            if (method.getSimpleName().contentEquals(copyOf) && method.getParameters().size() == 1
                    && method.getModifiers().contains(Modifier.STATIC)) {
                copyOfMethods.add(method);
            }
        }
        return copyOfMethods.build();
    }

    /**
     * Returns the return type of the given method from the builder. This should be the final type of
     * the method when any bound type variables are substituted. Consider this example:
     * <pre>{@code
     * abstract static class ParentBuilder<B extends ParentBuilder> {
     *   B setFoo(String s);
     * }
     * abstract static class ChildBuilder extends ParentBuilder<ChildBuilder> {
     *   ...
     * }
     * }</pre>
     * If the builder is {@code ChildBuilder} then the return type of {@code setFoo} is also
     * {@code ChildBuilder}, and not {@code B} as its {@code getReturnType()} method would claim.
     *
     * <p>If the caller is in a version of Eclipse with
     * <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=382590">this bug</a> then the
     * {@code asMemberOf} call will fail if the method is inherited from an interface. We work around
     * that for methods in the {@code @AutoValue} class using {@link EclipseHack#methodReturnTypes}
     * but we don't try to do so here because it should be much less likely. You might need to change
     * {@code ParentBuilder} from an interface to an abstract class to make it work, but you'll often
     * need to do that anyway.
     */
    TypeMirror builderMethodReturnType(ExecutableElement builderMethod) {
        DeclaredType builderTypeMirror = MoreTypes.asDeclared(builderType.asType());
        TypeMirror methodMirror;
        try {
            methodMirror = typeUtils.asMemberOf(builderTypeMirror, builderMethod);
        } catch (IllegalArgumentException e) {
            // Presumably we've hit the Eclipse bug cited.
            return builderMethod.getReturnType();
        }
        return MoreTypes.asExecutable(methodMirror).getReturnType();
    }

    private static String prefixWithSet(String propertyName) {
        // This is not internationalizationally correct, but it corresponds to what
        // Introspector.decapitalize does.
        return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
    }

    // Equivalent to Introspector.decapitalize but without the quirky exception whereby
    // Introspector.decapitalize("OAuth").equals("OAuth"). (If the first two letters are capitals
    // then Introspector.decapitalize does nothing.)
    private static String decapitalize(String propertyName) {
        return Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1);
    }

    private String typeParamsString() {
        return TypeSimplifier.actualTypeParametersString(autoValueClass);
    }
}