Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ffa0d6c9 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Annotation processing for int enum and flag mapping"

parents a64ea433 0b671da4
Loading
Loading
Loading
Loading
+81 −0
Original line number Diff line number Diff line
@@ -16,8 +16,12 @@

package android.processor.view.inspector;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
@@ -101,6 +105,83 @@ final class AnnotationUtils {
        return false;
    }

    /**
     * Get a typed list of values for an annotation array property by name.
     *
     * The returned list will be empty if the value was left at the default.
     *
     * @param propertyName The name of the property to search for
     * @param valueClass The expected class of the property value
     * @param element The element the annotation is on, used for exceptions
     * @param annotationMirror An annotation mirror to search for the property
     * @param <T> The type of the value
     * @return A list containing the requested types
     */
    <T> List<T> typedArrayValuesByName(
            String propertyName,
            Class<T> valueClass,
            Element element,
            AnnotationMirror annotationMirror) {
        return untypedArrayValuesByName(propertyName, element, annotationMirror)
                .stream()
                .map(annotationValue -> {
                    final Object value = annotationValue.getValue();

                    if (value == null) {
                        throw new ProcessingException(
                                "Unexpected null in array.",
                                element,
                                annotationMirror,
                                annotationValue);
                    }

                    if (valueClass.isAssignableFrom(value.getClass())) {
                        return valueClass.cast(value);
                    } else {
                        throw new ProcessingException(
                                String.format(
                                        "Expected array entry to have type %s, but got %s.",
                                        valueClass.getCanonicalName(),
                                        value.getClass().getCanonicalName()),
                                element,
                                annotationMirror,
                                annotationValue);
                    }
                })
                .collect(Collectors.toList());
    }

    /**
     * Get a list of values for an annotation array property by name.
     *
     * @param propertyName The name of the property to search for
     * @param element The element the annotation is on, used for exceptions
     * @param annotationMirror An annotation mirror to search for the property
     * @return A list of annotation values, empty list if none found
     */
    List<AnnotationValue> untypedArrayValuesByName(
            String propertyName,
            Element element,
            AnnotationMirror annotationMirror) {
        return typedValueByName(propertyName, List.class, element, annotationMirror)
                .map(untypedValues -> {
                    List<AnnotationValue> typedValues = new ArrayList<>(untypedValues.size());

                    for (Object untypedValue : untypedValues) {
                        if (untypedValue instanceof AnnotationValue) {
                            typedValues.add((AnnotationValue) untypedValue);
                        } else {
                            throw new ProcessingException(
                                    "Unable to convert array entry to AnnotationValue",
                                    element,
                                    annotationMirror);
                        }
                    }

                    return typedValues;
                }).orElseGet(Collections::emptyList);
    }

    /**
     * Get the typed value of an annotation property by name.
     *
+105 −0
Original line number Diff line number Diff line
@@ -19,7 +19,9 @@ package android.processor.view.inspector;
import com.squareup.javapoet.ClassName;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -92,6 +94,8 @@ public final class InspectableClassModel {
        private final Type mType;
        private boolean mAttributeIdInferrableFromR = true;
        private int mAttributeId = 0;
        private List<IntEnumEntry> mIntEnumEntries;
        private List<IntFlagEntry> mIntFlagEntries;

        public Property(String name, String getter, Type type) {
            mName = Objects.requireNonNull(name, "Name must not be null");
@@ -133,6 +137,40 @@ public final class InspectableClassModel {
            return mType;
        }

        /**
         * Get the mapping for an {@code int} enumeration, if present.
         *
         * @return A list of mapping entries, empty if absent
         */
        public List<IntEnumEntry> getIntEnumEntries() {
            if (mIntEnumEntries != null) {
                return mIntEnumEntries;
            } else {
                return Collections.emptyList();
            }
        }

        public void setIntEnumEntries(List<IntEnumEntry> intEnumEntries) {
            mIntEnumEntries = intEnumEntries;
        }

        /**
         * Get the mapping of {@code int} flags, if present.
         *
         * @return A list of mapping entries, empty if absent
         */
        public List<IntFlagEntry> getIntFlagEntries() {
            if (mIntFlagEntries != null) {
                return mIntFlagEntries;
            } else {
                return Collections.emptyList();
            }
        }

        public void setIntFlagEntries(List<IntFlagEntry> intFlagEntries) {
            mIntFlagEntries = intFlagEntries;
        }

        public enum Type {
            /** Primitive or boxed {@code boolean} */
            BOOLEAN,
@@ -181,6 +219,7 @@ public final class InspectableClassModel {
             * An enumeration packed into an {@code int}.
             *
             * @see android.view.inspector.IntEnumMapping
             * @see IntEnumEntry
             */
            INT_ENUM,

@@ -188,8 +227,74 @@ public final class InspectableClassModel {
             * Non-exclusive or partially-exclusive flags packed into an {@code int}.
             *
             * @see android.view.inspector.IntFlagMapping
             * @see IntFlagEntry
             */
            INT_FLAG
        }
    }

    /**
     * Model one entry in a int enum mapping.
     *
     * @see android.view.inspector.IntEnumMapping
     */
    public static final class IntEnumEntry {
        private final String mName;
        private final int mValue;

        public IntEnumEntry(String name, int value) {
            mName = Objects.requireNonNull(name, "Name must not be null");
            mValue = value;
        }

        public String getName() {
            return mName;
        }

        public int getValue() {
            return mValue;
        }
    }

    /**
     * Model one entry in an int flag mapping.
     *
     * @see android.view.inspector.IntFlagMapping
     */
    public static final class IntFlagEntry {
        private final String mName;
        private final int mTarget;
        private final int mMask;

        public IntFlagEntry(String name, int target, int mask) {
            mName = Objects.requireNonNull(name, "Name must not be null");
            mTarget = target;
            mMask = mask;
        }

        public IntFlagEntry(String name, int target) {
            this(name, target, target);
        }

        /**
         * Determine if this entry has a bitmask.
         *
         * @return True if the bitmask and target are different, false otherwise
         */
        public boolean hasMask() {
            return mTarget != mMask;
        }

        public String getName() {
            return mName;
        }

        public int getTarget() {
            return mTarget;
        }

        public int getMask() {
            return mMask;
        }
    }
}
+219 −19
Original line number Diff line number Diff line
@@ -16,13 +16,19 @@

package android.processor.view.inspector;

import android.processor.view.inspector.InspectableClassModel.IntEnumEntry;
import android.processor.view.inspector.InspectableClassModel.IntFlagEntry;
import android.processor.view.inspector.InspectableClassModel.Property;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -63,6 +69,7 @@ public final class InspectablePropertyProcessor implements ModelProcessor {

    /**
     * Set of android and androidx annotation qualified names for colors packed into {@code long}.
     *
     * @see android.annotation.ColorLong
     */
    private static final String[] COLOR_LONG_ANNOTATION_NAMES = {
@@ -109,8 +116,8 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
     * Check that an element is shaped like a getter.
     *
     * @param element An element that hopefully represents a getter
     * @throws ProcessingException if the element isn't a getter
     * @return An {@link ExecutableElement} that represents a getter method.
     * @throws ProcessingException if the element isn't a getter
     */
    private ExecutableElement ensureGetter(Element element) {
        if (element.getKind() != ElementKind.METHOD) {
@@ -169,8 +176,8 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
     *
     * @param getter     An element representing the getter to build from
     * @param annotation A mirror of an inspectable property-shaped annotation
     * @throws ProcessingException If the supplied data is invalid and a property cannot be modeled
     * @return A property for the getter and annotation
     * @throws ProcessingException If the supplied data is invalid and a property cannot be modeled
     */
    private Property buildProperty(ExecutableElement getter, AnnotationMirror annotation) {
        final String name = mAnnotationUtils
@@ -190,6 +197,15 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
                .typedValueByName("attributeId", Integer.class, getter, annotation)
                .ifPresent(property::setAttributeId);

        switch (property.getType()) {
            case INT_ENUM:
                property.setIntEnumEntries(processEnumMapping(getter, annotation));
                break;
            case INT_FLAG:
                property.setIntFlagEntries(processFlagMapping(getter, annotation));
                break;
        }

        return property;
    }

@@ -199,7 +215,7 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
     * @param getter     An element representing the getter to build from
     * @param annotation A mirror of an inspectable property-shaped annotation
     * @return The resolved property type
     * @throws ProcessingException If the property type cannot be resolved
     * @throws ProcessingException If the property type cannot be resolved or is invalid
     * @see android.view.inspector.InspectableProperty#valueType()
     */
    private Property.Type determinePropertyType(
@@ -213,10 +229,62 @@ public final class InspectablePropertyProcessor implements ModelProcessor {

        final Property.Type returnType = convertReturnTypeToPropertyType(getter);

        final boolean hasColor = hasColorAnnotation(getter);
        final Optional<AnnotationValue> enumMapping =
                mAnnotationUtils.valueByName("enumMapping", annotation);
        final Optional<AnnotationValue> flagMapping =
                mAnnotationUtils.valueByName("flagMapping", annotation);

        if (returnType != Property.Type.INT) {
            enumMapping.ifPresent(value -> {
                throw new ProcessingException(
                        String.format(
                                "Can only use enumMapping on int types, got %s.",
                                returnType.toString().toLowerCase()),
                        getter,
                        annotation,
                        value);
            });
            flagMapping.ifPresent(value -> {
                throw new ProcessingException(
                        String.format(
                                "Can only use flagMapping on int types, got %s.",
                                returnType.toString().toLowerCase()),
                        getter,
                        annotation,
                        value);
            });
        }

        switch (valueType) {
            case "INFERRED":
                if (hasColorAnnotation(getter)) {
                if (hasColor) {
                    enumMapping.ifPresent(value -> {
                        throw new ProcessingException(
                                "Cannot use enumMapping on a color type.",
                                getter,
                                annotation,
                                value);
                    });
                    flagMapping.ifPresent(value -> {
                        throw new ProcessingException(
                                "Cannot use flagMapping on a color type.",
                                getter,
                                annotation,
                                value);
                    });
                    return Property.Type.COLOR;
                } else if (enumMapping.isPresent()) {
                    flagMapping.ifPresent(value -> {
                        throw new ProcessingException(
                                "Cannot use flagMapping and enumMapping simultaneously.",
                                getter,
                                annotation,
                                value);
                    });
                    return Property.Type.INT_ENUM;
                } else if (flagMapping.isPresent()) {
                    return Property.Type.INT_FLAG;
                } else {
                    return returnType;
                }
@@ -235,17 +303,14 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
                                annotation);
                }
            case "GRAVITY":
                if (returnType == Property.Type.INT) {
                requirePackedIntToReturnInt("Gravity", returnType, getter, annotation);
                return Property.Type.GRAVITY;
                } else {
                    throw new ProcessingException(
                            String.format("Gravity must be an integer, got %s", returnType),
                            getter,
                            annotation);
                }
            case "INT_ENUM":
                requirePackedIntToReturnInt("IntEnum", returnType, getter, annotation);
                return Property.Type.INT_ENUM;
            case "INT_FLAG":
                throw new ProcessingException("Not implemented", getter, annotation);
                requirePackedIntToReturnInt("IntFlag", returnType, getter, annotation);
                return Property.Type.INT_FLAG;
            default:
                throw new ProcessingException(
                        String.format("Unknown value type enumeration value: %s", valueType),
@@ -258,8 +323,8 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
     * Get a property type from the return type of a getter.
     *
     * @param getter The getter to extract the return type of
     * @throws ProcessingException If the return type is not a primitive or an object
     * @return The property type returned by the getter
     * @throws ProcessingException If the return type is not a primitive or an object
     */
    private Property.Type convertReturnTypeToPropertyType(ExecutableElement getter) {
        final TypeMirror returnType = getter.getReturnType();
@@ -294,6 +359,31 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
        }
    }

    /**
     * Require that a value type packed into an integer be on a getter that returns an int.
     *
     * @param typeName   The name of the type to use in the exception
     * @param returnType The return type of the getter to check
     * @param getter     The getter, to use in the exception
     * @param annotation The annotation, to use in the exception
     * @throws ProcessingException If the return type is not an int
     */
    private static void requirePackedIntToReturnInt(
            String typeName,
            Property.Type returnType,
            ExecutableElement getter,
            AnnotationMirror annotation) {
        if (returnType != Property.Type.INT) {
            throw new ProcessingException(
                    String.format(
                            "%s can only be defined on a method that returns int, got %s.",
                            typeName,
                            returnType.toString().toLowerCase()),
                    getter,
                    annotation);
        }
    }

    /**
     * Determine if a getter is annotated with color annotation matching its return type.
     *
@@ -303,7 +393,6 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
     *
     * @param getter The getter to query
     * @return True if the getter has a color annotation, false otherwise
     *
     */
    private boolean hasColorAnnotation(ExecutableElement getter) {
        switch (unboxType(getter.getReturnType())) {
@@ -352,6 +441,117 @@ public final class InspectablePropertyProcessor implements ModelProcessor {
        }
    }

    /**
     * Build a model of an {@code int} enumeration mapping from annotation values.
     *
     * This method only handles the one-to-one mapping of mirrors of
     * {@link android.view.inspector.InspectableProperty.EnumMap} annotations into
     * {@link IntEnumEntry} objects. Further validation should be handled elsewhere
     *
     * @see android.view.inspector.IntEnumMapping
     * @see android.view.inspector.InspectableProperty#enumMapping()
     * @param getter The getter of the property, used for exceptions
     * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
     *                   extract enum mapping values from.
     * @return A list of int enum entries, in the order specified in source
     * @throws ProcessingException if mapping doesn't exist or is invalid
     */
    private List<IntEnumEntry> processEnumMapping(
            ExecutableElement getter,
            AnnotationMirror annotation) {
        List<AnnotationMirror> enumAnnotations = mAnnotationUtils.typedArrayValuesByName(
                "enumMapping", AnnotationMirror.class, getter, annotation);
        List<IntEnumEntry> enumEntries = new ArrayList<>(enumAnnotations.size());

        if (enumAnnotations.isEmpty()) {
            throw new ProcessingException(
                    "Encountered an empty array for enumMapping", getter, annotation);
        }

        for (AnnotationMirror enumAnnotation : enumAnnotations) {
            final String name = mAnnotationUtils.typedValueByName(
                    "name", String.class, getter, enumAnnotation)
                    .orElseThrow(() -> {
                        throw new ProcessingException(
                                "Name is required for @EnumMap",
                                getter,
                                enumAnnotation);
                    });

            final int value = mAnnotationUtils.typedValueByName(
                    "value", Integer.class, getter, enumAnnotation)
                    .orElseThrow(() -> {
                        throw new ProcessingException(
                                "Value is required for @EnumMap",
                                getter,
                                enumAnnotation);
                    });

            enumEntries.add(new IntEnumEntry(name, value));
        }

        return enumEntries;
    }

    /**
     * Build a model of an {@code int} flag mapping from annotation values.
     *
     * This method only handles the one-to-one mapping of mirrors of
     * {@link android.view.inspector.InspectableProperty.FlagMap} annotations into
     * {@link IntFlagEntry} objects. Further validation should be handled elsewhere
     *
     * @see android.view.inspector.IntFlagMapping
     * @see android.view.inspector.InspectableProperty#flagMapping()
     * @param getter The getter of the property, used for exceptions
     * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
     *                   extract flag mapping values from.
     * @return A list of int flags entries, in the order specified in source
     * @throws ProcessingException if mapping doesn't exist or is invalid
     */
    private List<IntFlagEntry> processFlagMapping(
            ExecutableElement getter,
            AnnotationMirror annotation) {
        List<AnnotationMirror> flagAnnotations = mAnnotationUtils.typedArrayValuesByName(
                "flagMapping", AnnotationMirror.class, getter, annotation);
        List<IntFlagEntry> flagEntries = new ArrayList<>(flagAnnotations.size());

        if (flagAnnotations.isEmpty()) {
            throw new ProcessingException(
                    "Encountered an empty array for flagMapping", getter, annotation);
        }

        for (AnnotationMirror flagAnnotation : flagAnnotations) {
            final String name = mAnnotationUtils.typedValueByName(
                    "name", String.class, getter, flagAnnotation)
                    .orElseThrow(() -> {
                        throw new ProcessingException(
                                "Name is required for @FlagMap",
                                getter,
                                flagAnnotation);
                    });

            final int target = mAnnotationUtils.typedValueByName(
                    "target", Integer.class, getter, flagAnnotation)
                    .orElseThrow(() -> {
                        throw new ProcessingException(
                                "Target is required for @FlagMap",
                                getter,
                                flagAnnotation);
                    });

            final Optional<Integer> mask = mAnnotationUtils.typedValueByName(
                    "mask", Integer.class, getter, flagAnnotation);

            if (mask.isPresent()) {
                flagEntries.add(new IntFlagEntry(name, target, mask.get()));
            } else {
                flagEntries.add(new IntFlagEntry(name, target));
            }
        }

        return flagEntries;
    }

    /**
     * Determine if a {@link TypeMirror} is a boxed or unboxed boolean.
     *
+77 −8

File changed.

Preview size limit exceeded, changes collapsed.

+7 −0
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;


@@ -118,6 +119,12 @@ public final class PlatformInspectableProcessor extends AbstractProcessor {
                break;
            }

            final Set<Modifier> classModifiers = classElement.get().getModifiers();

            if (classModifiers.contains(Modifier.PRIVATE)) {
                fail("Enclosing class cannot be private", element);
            }

            final InspectableClassModel model = modelMap.computeIfAbsent(
                    classElement.get().getQualifiedName().toString(),
                    k -> new InspectableClassModel(ClassName.get(classElement.get())));
Loading