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

Commit 0b671da4 authored by Ember Rose's avatar Ember Rose
Browse files

Annotation processing for int enum and flag mapping

Bug: 117616612
Test: atest --host view-inspector-annotation-processor-test
Change-Id: I791ffd8ce6bf6ec3ba408bb2a781fd91871b0ed6
parent e8d1eaa1
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