1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.processor.view.inspector;
18 
19 import android.processor.view.inspector.InspectableClassModel.Accessor;
20 import android.processor.view.inspector.InspectableClassModel.IntEnumEntry;
21 import android.processor.view.inspector.InspectableClassModel.IntFlagEntry;
22 import android.processor.view.inspector.InspectableClassModel.Property;
23 
24 import androidx.annotation.NonNull;
25 
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Optional;
29 import java.util.Set;
30 import java.util.regex.Pattern;
31 
32 import javax.annotation.processing.ProcessingEnvironment;
33 import javax.lang.model.element.AnnotationMirror;
34 import javax.lang.model.element.AnnotationValue;
35 import javax.lang.model.element.Element;
36 import javax.lang.model.element.ElementKind;
37 import javax.lang.model.element.ExecutableElement;
38 import javax.lang.model.element.Modifier;
39 import javax.lang.model.element.TypeElement;
40 import javax.lang.model.type.NoType;
41 import javax.lang.model.type.TypeKind;
42 import javax.lang.model.type.TypeMirror;
43 
44 /**
45  * Process {@code @InspectableProperty} annotations.
46  *
47  * @see android.view.inspector.InspectableProperty
48  */
49 public final class InspectablePropertyProcessor {
50     private final @NonNull String mQualifiedName;
51     private final @NonNull ProcessingEnvironment mProcessingEnv;
52     private final @NonNull AnnotationUtils mAnnotationUtils;
53 
54     /**
55      * Regex that matches methods names of the form {@code #getValue()}.
56      */
57     private static final Pattern GETTER_GET_PREFIX = Pattern.compile("\\Aget[A-Z]");
58 
59     /**
60      * Regex that matches method name of the form {@code #isPredicate()}.
61      */
62     private static final Pattern GETTER_IS_PREFIX = Pattern.compile("\\Ais[A-Z]");
63 
64     /**
65      * Set of android and androidx annotation qualified names for colors packed into {@code int}.
66      *
67      * @see android.annotation.ColorInt
68      */
69     private static final String[] COLOR_INT_ANNOTATION_NAMES = {
70             "android.annotation.ColorInt",
71             "androidx.annotation.ColorInt"};
72 
73     /**
74      * Set of android and androidx annotation qualified names for colors packed into {@code long}.
75      *
76      * @see android.annotation.ColorLong
77      */
78     private static final String[] COLOR_LONG_ANNOTATION_NAMES = {
79             "android.annotation.ColorLong",
80             "androidx.annotation.ColorLong"};
81 
82     /**
83      * Set of android and androidx annotation qualified names of resource ID annotations.
84      */
85     private static final String[] RESOURCE_ID_ANNOTATION_NAMES = {
86             "android.annotation.AnimatorRes",
87             "android.annotation.AnimRes",
88             "android.annotation.AnyRes",
89             "android.annotation.ArrayRes",
90             "android.annotation.BoolRes",
91             "android.annotation.DimenRes",
92             "android.annotation.DrawableRes",
93             "android.annotation.FontRes",
94             "android.annotation.IdRes",
95             "android.annotation.IntegerRes",
96             "android.annotation.InterpolatorRes",
97             "android.annotation.LayoutRes",
98             "android.annotation.MenuRes",
99             "android.annotation.NavigationRes",
100             "android.annotation.PluralsRes",
101             "android.annotation.RawRes",
102             "android.annotation.StringRes",
103             "android.annotation.StyleableRes",
104             "android.annotation.StyleRes",
105             "android.annotation.TransitionRes",
106             "android.annotation.XmlRes",
107             "androidx.annotation.AnimatorRes",
108             "androidx.annotation.AnimRes",
109             "androidx.annotation.AnyRes",
110             "androidx.annotation.ArrayRes",
111             "androidx.annotation.BoolRes",
112             "androidx.annotation.DimenRes",
113             "androidx.annotation.DrawableRes",
114             "androidx.annotation.FontRes",
115             "androidx.annotation.IdRes",
116             "androidx.annotation.IntegerRes",
117             "androidx.annotation.InterpolatorRes",
118             "androidx.annotation.LayoutRes",
119             "androidx.annotation.MenuRes",
120             "androidx.annotation.NavigationRes",
121             "androidx.annotation.PluralsRes",
122             "androidx.annotation.RawRes",
123             "androidx.annotation.StringRes",
124             "androidx.annotation.StyleableRes",
125             "androidx.annotation.StyleRes",
126             "androidx.annotation.TransitionRes",
127             "androidx.annotation.XmlRes"
128     };
129 
130     /**
131      * @param annotationQualifiedName The qualified name of the annotation to process
132      * @param processingEnv           The processing environment from the parent processor
133      */
InspectablePropertyProcessor( @onNull String annotationQualifiedName, @NonNull ProcessingEnvironment processingEnv)134     public InspectablePropertyProcessor(
135             @NonNull String annotationQualifiedName,
136             @NonNull ProcessingEnvironment processingEnv) {
137         mQualifiedName = annotationQualifiedName;
138         mProcessingEnv = processingEnv;
139         mAnnotationUtils = new AnnotationUtils(processingEnv);
140     }
141 
process(@onNull Element element, @NonNull InspectableClassModel model)142     public void process(@NonNull Element element, @NonNull InspectableClassModel model) {
143         try {
144             final AnnotationMirror annotation =
145                     mAnnotationUtils.exactlyOneMirror(mQualifiedName, element);
146             final Property property = buildProperty(element, annotation);
147 
148             model.getProperty(property.getName()).ifPresent(p -> {
149                 throw new ProcessingException(
150                         String.format(
151                                 "Property \"%s\" is already defined on #%s.",
152                                 p.getName(),
153                                 p.getAccessor().invocation()),
154                         element,
155                         annotation);
156             });
157 
158             model.putProperty(property);
159         } catch (ProcessingException processingException) {
160             processingException.print(mProcessingEnv.getMessager());
161         }
162     }
163 
164 
165     /**
166      * Build a {@link Property} from a getter and an inspectable property annotation.
167      *
168      * @param accessor An element representing the getter or public field to build from
169      * @param annotation A mirror of an inspectable property-shaped annotation
170      * @return A property for the getter and annotation
171      * @throws ProcessingException If the supplied data is invalid and a property cannot be modeled
172      */
173     @NonNull
buildProperty( @onNull Element accessor, @NonNull AnnotationMirror annotation)174     private Property buildProperty(
175             @NonNull Element accessor,
176             @NonNull AnnotationMirror annotation) {
177         final Property property;
178         final Optional<String> nameFromAnnotation = mAnnotationUtils
179                 .typedValueByName("name", String.class, accessor, annotation);
180 
181         validateModifiers(accessor);
182 
183         switch (accessor.getKind()) {
184             case FIELD:
185                 property = new Property(
186                         nameFromAnnotation.orElseGet(() -> accessor.getSimpleName().toString()),
187                         Accessor.ofField(accessor.getSimpleName().toString()),
188                         determinePropertyType(accessor, annotation));
189                 break;
190             case METHOD:
191                 final ExecutableElement getter = ensureGetter(accessor);
192 
193                 property = new Property(
194                         nameFromAnnotation.orElseGet(() -> inferPropertyNameFromGetter(getter)),
195                         Accessor.ofGetter(getter.getSimpleName().toString()),
196                         determinePropertyType(getter, annotation));
197                 break;
198             default:
199                 throw new ProcessingException(
200                         String.format(
201                                 "Property must either be a getter method or a field, got %s.",
202                                 accessor.getKind()
203                         ),
204                         accessor,
205                         annotation);
206         }
207 
208         mAnnotationUtils
209                 .typedValueByName("hasAttributeId", Boolean.class, accessor, annotation)
210                 .ifPresent(property::setAttributeIdInferrableFromR);
211 
212         mAnnotationUtils
213                 .typedValueByName("attributeId", Integer.class, accessor, annotation)
214                 .ifPresent(property::setAttributeId);
215 
216         switch (property.getType()) {
217             case INT_ENUM:
218                 property.setIntEnumEntries(processEnumMapping(accessor, annotation));
219                 break;
220             case INT_FLAG:
221                 property.setIntFlagEntries(processFlagMapping(accessor, annotation));
222                 break;
223         }
224 
225         return property;
226     }
227 
228     /**
229      * Validates that an element is public, concrete, and non-static.
230      *
231      * @param element The element to check
232      * @throws ProcessingException If the element's modifiers are invalid
233      */
validateModifiers(@onNull Element element)234     private void validateModifiers(@NonNull Element element) {
235         final Set<Modifier> modifiers = element.getModifiers();
236 
237         if (!modifiers.contains(Modifier.PUBLIC)) {
238             throw new ProcessingException(
239                     "Property getter methods and fields must be public.",
240                     element);
241         }
242 
243         if (modifiers.contains(Modifier.ABSTRACT)) {
244             throw new ProcessingException(
245                     "Property getter methods must not be abstract.",
246                     element);
247         }
248 
249         if (modifiers.contains(Modifier.STATIC)) {
250             throw new ProcessingException(
251                     "Property getter methods and fields must not be static.",
252                     element);
253         }
254     }
255 
256     /**
257      * Check that an element is shaped like a getter.
258      *
259      * @param element An element that hopefully represents a getter
260      * @return An {@link ExecutableElement} that represents a getter method.
261      * @throws ProcessingException if the element isn't a getter
262      */
263     @NonNull
ensureGetter(@onNull Element element)264     private ExecutableElement ensureGetter(@NonNull Element element) {
265         if (element.getKind() != ElementKind.METHOD) {
266             throw new ProcessingException(
267                     String.format("Expected a method, got a %s", element.getKind()),
268                     element);
269         }
270 
271         final ExecutableElement method = (ExecutableElement) element;
272 
273 
274         if (!method.getParameters().isEmpty()) {
275             throw new ProcessingException(
276                     String.format(
277                             "Expected a getter method to take no parameters, "
278                                     + "but got %d parameters.",
279                             method.getParameters().size()),
280                     element);
281         }
282 
283         if (method.isVarArgs()) {
284             throw new ProcessingException(
285                     "Expected a getter method to take no arguments, but got a var args method.",
286                     element);
287         }
288 
289         if (method.getReturnType() instanceof NoType) {
290             throw new ProcessingException(
291                     "Expected a getter to have a return type, got void.",
292                     element);
293         }
294 
295         return method;
296     }
297 
298 
299     /**
300      * Determine the property type from the annotation, return type, or context clues.
301      *
302      * @param accessor An element representing the getter or field to determine the type of
303      * @param annotation A mirror of an inspectable property-shaped annotation
304      * @return The resolved property type
305      * @throws ProcessingException If the property type cannot be resolved or is invalid
306      * @see android.view.inspector.InspectableProperty#valueType()
307      */
308     @NonNull
determinePropertyType( @onNull Element accessor, @NonNull AnnotationMirror annotation)309     private Property.Type determinePropertyType(
310             @NonNull Element accessor,
311             @NonNull AnnotationMirror annotation) {
312         final String valueType = mAnnotationUtils
313                 .untypedValueByName("valueType", accessor, annotation)
314                 .map(Object::toString)
315                 .orElse("INFERRED");
316 
317         final Property.Type accessorType =
318                 convertTypeMirrorToPropertyType(extractReturnOrFieldType(accessor), accessor);
319 
320         final Optional<AnnotationValue> enumMapping =
321                 mAnnotationUtils.valueByName("enumMapping", annotation);
322         final Optional<AnnotationValue> flagMapping =
323                 mAnnotationUtils.valueByName("flagMapping", annotation);
324 
325         if (accessorType != Property.Type.INT) {
326             enumMapping.ifPresent(value -> {
327                 throw new ProcessingException(
328                         String.format(
329                                 "Can only use enumMapping on int types, got %s.",
330                                 accessorType.toString().toLowerCase()),
331                         accessor,
332                         annotation,
333                         value);
334             });
335             flagMapping.ifPresent(value -> {
336                 throw new ProcessingException(
337                         String.format(
338                                 "Can only use flagMapping on int types, got %s.",
339                                 accessorType.toString().toLowerCase()),
340                         accessor,
341                         annotation,
342                         value);
343             });
344         }
345 
346 
347         switch (valueType) {
348             case "INFERRED":
349                 final boolean hasColor = hasColorAnnotation(accessor);
350                 final boolean hasResourceId = hasResourceIdAnnotation(accessor);
351 
352                 if (hasColor) {
353                     enumMapping.ifPresent(value -> {
354                         throw new ProcessingException(
355                                 "Cannot use enumMapping on a color type.",
356                                 accessor,
357                                 annotation,
358                                 value);
359                     });
360                     flagMapping.ifPresent(value -> {
361                         throw new ProcessingException(
362                                 "Cannot use flagMapping on a color type.",
363                                 accessor,
364                                 annotation,
365                                 value);
366                     });
367                     if (hasResourceId) {
368                         throw new ProcessingException(
369                                 "Cannot infer type, both color and resource ID annotations "
370                                         + "are present.",
371                                 accessor,
372                                 annotation);
373                     }
374                     return Property.Type.COLOR;
375                 } else if (hasResourceId) {
376                     enumMapping.ifPresent(value -> {
377                         throw new ProcessingException(
378                                 "Cannot use enumMapping on a resource ID type.",
379                                 accessor,
380                                 annotation,
381                                 value);
382                     });
383                     flagMapping.ifPresent(value -> {
384                         throw new ProcessingException(
385                                 "Cannot use flagMapping on a resource ID type.",
386                                 accessor,
387                                 annotation,
388                                 value);
389                     });
390                     return Property.Type.RESOURCE_ID;
391                 } else if (enumMapping.isPresent()) {
392                     flagMapping.ifPresent(value -> {
393                         throw new ProcessingException(
394                                 "Cannot use flagMapping and enumMapping simultaneously.",
395                                 accessor,
396                                 annotation,
397                                 value);
398                     });
399                     return Property.Type.INT_ENUM;
400                 } else if (flagMapping.isPresent()) {
401                     return Property.Type.INT_FLAG;
402                 } else {
403                     return accessorType;
404                 }
405             case "NONE":
406                 return accessorType;
407             case "COLOR":
408                 switch (accessorType) {
409                     case COLOR:
410                     case INT:
411                     case LONG:
412                         return Property.Type.COLOR;
413                     default:
414                         throw new ProcessingException(
415                                 "Color must be a long, integer, or android.graphics.Color",
416                                 accessor,
417                                 annotation);
418                 }
419             case "GRAVITY":
420                 requirePackedIntToBeInt("Gravity", accessorType, accessor, annotation);
421                 return Property.Type.GRAVITY;
422             case "INT_ENUM":
423                 requirePackedIntToBeInt("IntEnum", accessorType, accessor, annotation);
424                 return Property.Type.INT_ENUM;
425             case "INT_FLAG":
426                 requirePackedIntToBeInt("IntFlag", accessorType, accessor, annotation);
427                 return Property.Type.INT_FLAG;
428             case "RESOURCE_ID":
429                 return Property.Type.RESOURCE_ID;
430             default:
431                 throw new ProcessingException(
432                         String.format("Unknown value type enumeration value: %s", valueType),
433                         accessor,
434                         annotation);
435         }
436     }
437 
438     /**
439      * Get the type of a field or the return type of a method.
440      *
441      * @param element The element to extract a {@link TypeMirror} from
442      * @return The return or field type of the element
443      * @throws ProcessingException If the element is not a field or a method
444      */
445     @NonNull
extractReturnOrFieldType(@onNull Element element)446     private TypeMirror extractReturnOrFieldType(@NonNull Element element) {
447         switch (element.getKind()) {
448             case FIELD:
449                 return element.asType();
450             case METHOD:
451                 return ((ExecutableElement) element).getReturnType();
452             default:
453                 throw new ProcessingException(
454                         String.format(
455                                 "Unable to determine the type of a %s.",
456                                 element.getKind()),
457                         element);
458         }
459     }
460 
461     /**
462      * Get a property type from a type mirror
463      *
464      * @param typeMirror The type mirror to convert to a property type
465      * @param element The element to be used for exceptions
466      * @return The property type returned by the getter
467      * @throws ProcessingException If the return type is not a primitive or an object
468      */
469     @NonNull
convertTypeMirrorToPropertyType( @onNull TypeMirror typeMirror, @NonNull Element element)470     private Property.Type convertTypeMirrorToPropertyType(
471             @NonNull TypeMirror typeMirror,
472             @NonNull Element element) {
473         switch (unboxType(typeMirror)) {
474             case BOOLEAN:
475                 return Property.Type.BOOLEAN;
476             case BYTE:
477                 return Property.Type.BYTE;
478             case CHAR:
479                 return Property.Type.CHAR;
480             case DOUBLE:
481                 return Property.Type.DOUBLE;
482             case FLOAT:
483                 return Property.Type.FLOAT;
484             case INT:
485                 return Property.Type.INT;
486             case LONG:
487                 return Property.Type.LONG;
488             case SHORT:
489                 return Property.Type.SHORT;
490             case DECLARED:
491                 if (isColorType(typeMirror)) {
492                     return Property.Type.COLOR;
493                 } else {
494                     return Property.Type.OBJECT;
495                 }
496             case ARRAY:
497                 return Property.Type.OBJECT;
498             default:
499                 throw new ProcessingException(
500                         String.format("Unsupported property type %s.", typeMirror),
501                         element);
502         }
503     }
504 
505     /**
506      * Require that a value type packed into an integer be on a getter that returns an int.
507      *
508      * @param typeName The name of the type to use in the exception
509      * @param returnType The return type of the getter to check
510      * @param accessor The getter, to use in the exception
511      * @param annotation The annotation, to use in the exception
512      * @throws ProcessingException If the return type is not an int
513      */
requirePackedIntToBeInt( @onNull String typeName, @NonNull Property.Type returnType, @NonNull Element accessor, @NonNull AnnotationMirror annotation)514     private static void requirePackedIntToBeInt(
515             @NonNull String typeName,
516             @NonNull Property.Type returnType,
517             @NonNull Element accessor,
518             @NonNull AnnotationMirror annotation) {
519         if (returnType != Property.Type.INT) {
520             throw new ProcessingException(
521                     String.format(
522                             "%s can only be defined on a method that returns int, got %s.",
523                             typeName,
524                             returnType.toString().toLowerCase()),
525                     accessor,
526                     annotation);
527         }
528     }
529 
530     /**
531      * Determine if a getter is annotated with color annotation matching its return type.
532      *
533      * Note that an {@code int} return value annotated with {@link android.annotation.ColorLong} is
534      * not considered to be annotated, nor is a {@code long} annotated with
535      * {@link android.annotation.ColorInt}.
536      *
537      * @param accessor The getter or field to query
538      * @return True if the getter has a color annotation, false otherwise
539      */
hasColorAnnotation(@onNull Element accessor)540     private boolean hasColorAnnotation(@NonNull Element accessor) {
541         switch (unboxType(extractReturnOrFieldType(accessor))) {
542             case INT:
543                 for (String name : COLOR_INT_ANNOTATION_NAMES) {
544                     if (mAnnotationUtils.hasAnnotation(accessor, name)) {
545                         return true;
546                     }
547                 }
548                 return false;
549             case LONG:
550                 for (String name : COLOR_LONG_ANNOTATION_NAMES) {
551                     if (mAnnotationUtils.hasAnnotation(accessor, name)) {
552                         return true;
553                     }
554                 }
555                 return false;
556             default:
557                 return false;
558         }
559     }
560 
561     /**
562      * Determine if a getter or a field is annotated with a resource ID annotation.
563      *
564      * @param accessor The getter or field to query
565      * @return True if the accessor is an integer and has a resource ID annotation, false otherwise
566      */
hasResourceIdAnnotation(@onNull Element accessor)567     private boolean hasResourceIdAnnotation(@NonNull Element accessor) {
568         if (unboxType(extractReturnOrFieldType(accessor)) == TypeKind.INT) {
569             for (String name : RESOURCE_ID_ANNOTATION_NAMES) {
570                 if (mAnnotationUtils.hasAnnotation(accessor, name)) {
571                     return true;
572                 }
573             }
574         }
575 
576         return false;
577     }
578 
579     /**
580      * Infer a property name from a getter method.
581      *
582      * If the method is prefixed with {@code get}, the prefix will be stripped, and the
583      * capitalization fixed. E.g.: {@code getSomeProperty} to {@code someProperty}.
584      *
585      * Additionally, if the method's return type is a boolean, an {@code is} prefix will also be
586      * stripped. E.g.: {@code isPropertyEnabled} to {@code propertyEnabled}.
587      *
588      * Failing that, this method will just return the full name of the getter.
589      *
590      * @param getter An element representing a getter
591      * @return A string property name
592      */
593     @NonNull
inferPropertyNameFromGetter(@onNull ExecutableElement getter)594     private String inferPropertyNameFromGetter(@NonNull ExecutableElement getter) {
595         final String name = getter.getSimpleName().toString();
596 
597         if (GETTER_GET_PREFIX.matcher(name).find()) {
598             return name.substring(3, 4).toLowerCase() + name.substring(4);
599         } else if (isBoolean(getter.getReturnType()) && GETTER_IS_PREFIX.matcher(name).find()) {
600             return name.substring(2, 3).toLowerCase() + name.substring(3);
601         } else {
602             return name;
603         }
604     }
605 
606     /**
607      * Build a model of an {@code int} enumeration mapping from annotation values.
608      *
609      * This method only handles the one-to-one mapping of mirrors of
610      * {@link android.view.inspector.InspectableProperty.EnumEntry} annotations into
611      * {@link IntEnumEntry} objects. Further validation should be handled elsewhere
612      *
613      * @see android.view.inspector.InspectableProperty#enumMapping()
614      * @param accessor The accessor of the property, used for exceptions
615      * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
616      *                   extract enum mapping values from.
617      * @return A list of int enum entries, in the order specified in source
618      * @throws ProcessingException if mapping doesn't exist or is invalid
619      */
620     @NonNull
processEnumMapping( @onNull Element accessor, @NonNull AnnotationMirror annotation)621     private List<IntEnumEntry> processEnumMapping(
622             @NonNull Element accessor,
623             @NonNull AnnotationMirror annotation) {
624         List<AnnotationMirror> enumAnnotations = mAnnotationUtils.typedArrayValuesByName(
625                 "enumMapping", AnnotationMirror.class, accessor, annotation);
626         List<IntEnumEntry> enumEntries = new ArrayList<>(enumAnnotations.size());
627 
628         if (enumAnnotations.isEmpty()) {
629             throw new ProcessingException(
630                     "Encountered an empty array for enumMapping", accessor, annotation);
631         }
632 
633         for (AnnotationMirror enumAnnotation : enumAnnotations) {
634             final String name = mAnnotationUtils.typedValueByName(
635                     "name", String.class, accessor, enumAnnotation)
636                     .orElseThrow(() -> new ProcessingException(
637                             "Name is required for @EnumEntry",
638                             accessor,
639                             enumAnnotation));
640 
641             final int value = mAnnotationUtils.typedValueByName(
642                     "value", Integer.class, accessor, enumAnnotation)
643                     .orElseThrow(() -> new ProcessingException(
644                             "Value is required for @EnumEntry",
645                             accessor,
646                             enumAnnotation));
647 
648             enumEntries.add(new IntEnumEntry(value, name));
649         }
650 
651         return enumEntries;
652     }
653 
654     /**
655      * Build a model of an {@code int} flag mapping from annotation values.
656      *
657      * This method only handles the one-to-one mapping of mirrors of
658      * {@link android.view.inspector.InspectableProperty.FlagEntry} annotations into
659      * {@link IntFlagEntry} objects. Further validation should be handled elsewhere
660      *
661      * @see android.view.inspector.IntFlagMapping
662      * @see android.view.inspector.InspectableProperty#flagMapping()
663      * @param accessor The accessor of the property, used for exceptions
664      * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
665      *                   extract flag mapping values from.
666      * @return A list of int flags entries, in the order specified in source
667      * @throws ProcessingException if mapping doesn't exist or is invalid
668      */
669     @NonNull
processFlagMapping( @onNull Element accessor, @NonNull AnnotationMirror annotation)670     private List<IntFlagEntry> processFlagMapping(
671             @NonNull Element accessor,
672             @NonNull AnnotationMirror annotation) {
673         List<AnnotationMirror> flagAnnotations = mAnnotationUtils.typedArrayValuesByName(
674                 "flagMapping", AnnotationMirror.class, accessor, annotation);
675         List<IntFlagEntry> flagEntries = new ArrayList<>(flagAnnotations.size());
676 
677         if (flagAnnotations.isEmpty()) {
678             throw new ProcessingException(
679                     "Encountered an empty array for flagMapping", accessor, annotation);
680         }
681 
682         for (AnnotationMirror flagAnnotation : flagAnnotations) {
683             final String name = mAnnotationUtils.typedValueByName(
684                     "name", String.class, accessor, flagAnnotation)
685                     .orElseThrow(() -> new ProcessingException(
686                             "Name is required for @FlagEntry",
687                             accessor,
688                             flagAnnotation));
689 
690             final int target = mAnnotationUtils.typedValueByName(
691                     "target", Integer.class, accessor, flagAnnotation)
692                     .orElseThrow(() -> new ProcessingException(
693                             "Target is required for @FlagEntry",
694                             accessor,
695                             flagAnnotation));
696 
697             final Optional<Integer> mask = mAnnotationUtils.typedValueByName(
698                     "mask", Integer.class, accessor, flagAnnotation);
699 
700             if (mask.isPresent()) {
701                 flagEntries.add(new IntFlagEntry(mask.get(), target, name));
702             } else {
703                 flagEntries.add(new IntFlagEntry(target, name));
704             }
705         }
706 
707         return flagEntries;
708     }
709 
710     /**
711      * Determine if a {@link TypeMirror} is a boxed or unboxed boolean.
712      *
713      * @param type The type mirror to check
714      * @return True if the type is a boolean
715      */
isBoolean(@onNull TypeMirror type)716     private boolean isBoolean(@NonNull TypeMirror type) {
717         if (type.getKind() == TypeKind.DECLARED) {
718             return mProcessingEnv.getTypeUtils().unboxedType(type).getKind() == TypeKind.BOOLEAN;
719         } else {
720             return type.getKind() == TypeKind.BOOLEAN;
721         }
722     }
723 
724     /**
725      * Unbox a type mirror if it represents a boxed type, otherwise pass it through.
726      *
727      * @param typeMirror The type mirror to unbox
728      * @return The same type mirror, or an unboxed primitive version
729      */
730     @NonNull
unboxType(@onNull TypeMirror typeMirror)731     private TypeKind unboxType(@NonNull TypeMirror typeMirror) {
732         final TypeKind typeKind = typeMirror.getKind();
733 
734         if (typeKind.isPrimitive()) {
735             return typeKind;
736         } else if (typeKind == TypeKind.DECLARED) {
737             try {
738                 return mProcessingEnv.getTypeUtils().unboxedType(typeMirror).getKind();
739             } catch (IllegalArgumentException e) {
740                 return typeKind;
741             }
742         } else {
743             return typeKind;
744         }
745     }
746 
747     /**
748      * Determine if a type mirror represents a subtype of {@link android.graphics.Color}.
749      *
750      * @param typeMirror The type mirror to test
751      * @return True if it represents a subclass of color, false otherwise
752      */
isColorType(@onNull TypeMirror typeMirror)753     private boolean isColorType(@NonNull TypeMirror typeMirror) {
754         final TypeElement colorType = mProcessingEnv
755                 .getElementUtils()
756                 .getTypeElement("android.graphics.Color");
757 
758         if (colorType == null) {
759             return false;
760         } else {
761             return mProcessingEnv.getTypeUtils().isSubtype(typeMirror, colorType.asType());
762         }
763     }
764 }
765