blob: 20de90d51482c1ffe207df357a5e3c22562291e3 [file] [log] [blame]
Ashley Rosec1a4dec2018-12-13 18:06:30 -05001/*
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
17package android.processor.view.inspector;
18
Ashley Rose89d6bce2019-03-01 19:24:50 -050019import android.processor.view.inspector.InspectableClassModel.Accessor;
Ashley Rose0b671da2019-01-25 15:41:29 -050020import android.processor.view.inspector.InspectableClassModel.IntEnumEntry;
21import android.processor.view.inspector.InspectableClassModel.IntFlagEntry;
Ashley Rosec1a4dec2018-12-13 18:06:30 -050022import android.processor.view.inspector.InspectableClassModel.Property;
23
Ashley Rose0b671da2019-01-25 15:41:29 -050024import java.util.ArrayList;
25import java.util.List;
26import java.util.Optional;
Ashley Rosec1a4dec2018-12-13 18:06:30 -050027import java.util.Set;
28import java.util.regex.Pattern;
29
30import javax.annotation.processing.ProcessingEnvironment;
31import javax.lang.model.element.AnnotationMirror;
Ashley Rose0b671da2019-01-25 15:41:29 -050032import javax.lang.model.element.AnnotationValue;
Ashley Rosec1a4dec2018-12-13 18:06:30 -050033import javax.lang.model.element.Element;
34import javax.lang.model.element.ElementKind;
35import javax.lang.model.element.ExecutableElement;
36import javax.lang.model.element.Modifier;
37import javax.lang.model.element.TypeElement;
38import javax.lang.model.type.NoType;
39import javax.lang.model.type.TypeKind;
40import javax.lang.model.type.TypeMirror;
41
42/**
43 * Process {@code @InspectableProperty} annotations.
44 *
45 * @see android.view.inspector.InspectableProperty
46 */
47public final class InspectablePropertyProcessor implements ModelProcessor {
48 private final String mQualifiedName;
49 private final ProcessingEnvironment mProcessingEnv;
50 private final AnnotationUtils mAnnotationUtils;
51
52 /**
53 * Regex that matches methods names of the form {@code #getValue()}.
54 */
55 private static final Pattern GETTER_GET_PREFIX = Pattern.compile("\\Aget[A-Z]");
56
57 /**
58 * Regex that matches method name of the form {@code #isPredicate()}.
59 */
60 private static final Pattern GETTER_IS_PREFIX = Pattern.compile("\\Ais[A-Z]");
61
62 /**
63 * Set of android and androidx annotation qualified names for colors packed into {@code int}.
64 *
65 * @see android.annotation.ColorInt
66 */
67 private static final String[] COLOR_INT_ANNOTATION_NAMES = {
68 "android.annotation.ColorInt",
69 "androidx.annotation.ColorInt"};
70
71 /**
72 * Set of android and androidx annotation qualified names for colors packed into {@code long}.
Ashley Rose0b671da2019-01-25 15:41:29 -050073 *
Ashley Rosec1a4dec2018-12-13 18:06:30 -050074 * @see android.annotation.ColorLong
75 */
76 private static final String[] COLOR_LONG_ANNOTATION_NAMES = {
77 "android.annotation.ColorLong",
78 "androidx.annotation.ColorLong"};
79
80 /**
Ashley Rosee8914812019-03-05 17:12:00 -050081 * Set of android and androidx annotation qualified names of resource ID annotations.
82 */
83 private static final String[] RESOURCE_ID_ANNOTATION_NAMES = {
84 "android.annotation.AnimatorRes",
85 "android.annotation.AnimRes",
86 "android.annotation.AnyRes",
87 "android.annotation.ArrayRes",
88 "android.annotation.BoolRes",
89 "android.annotation.DimenRes",
90 "android.annotation.DrawableRes",
91 "android.annotation.FontRes",
92 "android.annotation.IdRes",
93 "android.annotation.IntegerRes",
94 "android.annotation.InterpolatorRes",
95 "android.annotation.LayoutRes",
96 "android.annotation.MenuRes",
97 "android.annotation.NavigationRes",
98 "android.annotation.PluralsRes",
99 "android.annotation.RawRes",
100 "android.annotation.StringRes",
101 "android.annotation.StyleableRes",
102 "android.annotation.StyleRes",
103 "android.annotation.TransitionRes",
104 "android.annotation.XmlRes",
105 "androidx.annotation.AnimatorRes",
106 "androidx.annotation.AnimRes",
107 "androidx.annotation.AnyRes",
108 "androidx.annotation.ArrayRes",
109 "androidx.annotation.BoolRes",
110 "androidx.annotation.DimenRes",
111 "androidx.annotation.DrawableRes",
112 "androidx.annotation.FontRes",
113 "androidx.annotation.IdRes",
114 "androidx.annotation.IntegerRes",
115 "androidx.annotation.InterpolatorRes",
116 "androidx.annotation.LayoutRes",
117 "androidx.annotation.MenuRes",
118 "androidx.annotation.NavigationRes",
119 "androidx.annotation.PluralsRes",
120 "androidx.annotation.RawRes",
121 "androidx.annotation.StringRes",
122 "androidx.annotation.StyleableRes",
123 "androidx.annotation.StyleRes",
124 "androidx.annotation.TransitionRes",
125 "androidx.annotation.XmlRes"
126 };
127
128 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500129 * @param annotationQualifiedName The qualified name of the annotation to process
Ashley Rose0b671da2019-01-25 15:41:29 -0500130 * @param processingEnv The processing environment from the parent processor
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500131 */
132 public InspectablePropertyProcessor(
133 String annotationQualifiedName,
134 ProcessingEnvironment processingEnv) {
135 mQualifiedName = annotationQualifiedName;
136 mProcessingEnv = processingEnv;
137 mAnnotationUtils = new AnnotationUtils(processingEnv);
138 }
139
140 @Override
141 public void process(Element element, InspectableClassModel model) {
142 try {
143 final AnnotationMirror annotation =
144 mAnnotationUtils.exactlyOneMirror(mQualifiedName, element);
Ashley Rose89d6bce2019-03-01 19:24:50 -0500145 final Property property = buildProperty(element, annotation);
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500146
147 model.getProperty(property.getName()).ifPresent(p -> {
148 throw new ProcessingException(
149 String.format(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500150 "Property \"%s\" is already defined on #%s.",
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500151 p.getName(),
Ashley Rose89d6bce2019-03-01 19:24:50 -0500152 p.getAccessor().invocation()),
153 element,
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500154 annotation);
155 });
156
157 model.putProperty(property);
158 } catch (ProcessingException processingException) {
159 processingException.print(mProcessingEnv.getMessager());
160 }
161 }
162
Ashley Rose89d6bce2019-03-01 19:24:50 -0500163
164 /**
165 * Build a {@link Property} from a getter and an inspectable property annotation.
166 *
167 * @param accessor An element representing the getter or public field to build from
168 * @param annotation A mirror of an inspectable property-shaped annotation
169 * @return A property for the getter and annotation
170 * @throws ProcessingException If the supplied data is invalid and a property cannot be modeled
171 */
172 private Property buildProperty(Element accessor, AnnotationMirror annotation) {
173 final Property property;
174 final Optional<String> nameFromAnnotation = mAnnotationUtils
175 .typedValueByName("name", String.class, accessor, annotation);
176
177 validateModifiers(accessor);
178
179 switch (accessor.getKind()) {
180 case FIELD:
181 property = new Property(
182 nameFromAnnotation.orElseGet(() -> accessor.getSimpleName().toString()),
183 Accessor.ofField(accessor.getSimpleName().toString()),
184 determinePropertyType(accessor, annotation));
185 break;
186 case METHOD:
187 final ExecutableElement getter = ensureGetter(accessor);
188
189 property = new Property(
190 nameFromAnnotation.orElseGet(() -> inferPropertyNameFromGetter(getter)),
191 Accessor.ofGetter(getter.getSimpleName().toString()),
192 determinePropertyType(getter, annotation));
193 break;
194 default:
195 throw new ProcessingException(
196 String.format(
197 "Property must either be a getter method or a field, got %s.",
198 accessor.getKind()
199 ),
200 accessor,
201 annotation);
202 }
203
204 mAnnotationUtils
205 .typedValueByName("hasAttributeId", Boolean.class, accessor, annotation)
206 .ifPresent(property::setAttributeIdInferrableFromR);
207
208 mAnnotationUtils
209 .typedValueByName("attributeId", Integer.class, accessor, annotation)
210 .ifPresent(property::setAttributeId);
211
212 switch (property.getType()) {
213 case INT_ENUM:
214 property.setIntEnumEntries(processEnumMapping(accessor, annotation));
215 break;
216 case INT_FLAG:
217 property.setIntFlagEntries(processFlagMapping(accessor, annotation));
218 break;
219 }
220
221 return property;
222 }
223
224 /**
225 * Validates that an element is public, concrete, and non-static.
226 *
227 * @param element The element to check
228 * @throws ProcessingException If the element's modifiers are invalid
229 */
230 private void validateModifiers(Element element) {
231 final Set<Modifier> modifiers = element.getModifiers();
232
233 if (!modifiers.contains(Modifier.PUBLIC)) {
234 throw new ProcessingException(
235 "Property getter methods and fields must be public.",
236 element);
237 }
238
239 if (modifiers.contains(Modifier.ABSTRACT)) {
240 throw new ProcessingException(
241 "Property getter methods must not be abstract.",
242 element);
243 }
244
245 if (modifiers.contains(Modifier.STATIC)) {
246 throw new ProcessingException(
247 "Property getter methods and fields must not be static.",
248 element);
249 }
250 }
251
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500252 /**
253 * Check that an element is shaped like a getter.
254 *
255 * @param element An element that hopefully represents a getter
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500256 * @return An {@link ExecutableElement} that represents a getter method.
Ashley Rose0b671da2019-01-25 15:41:29 -0500257 * @throws ProcessingException if the element isn't a getter
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500258 */
259 private ExecutableElement ensureGetter(Element element) {
260 if (element.getKind() != ElementKind.METHOD) {
261 throw new ProcessingException(
262 String.format("Expected a method, got a %s", element.getKind()),
263 element);
264 }
265
266 final ExecutableElement method = (ExecutableElement) element;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500267
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500268
269 if (!method.getParameters().isEmpty()) {
270 throw new ProcessingException(
271 String.format(
272 "Expected a getter method to take no parameters, "
Ashley Rose0b671da2019-01-25 15:41:29 -0500273 + "but got %d parameters.",
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500274 method.getParameters().size()),
275 element);
276 }
277
278 if (method.isVarArgs()) {
279 throw new ProcessingException(
280 "Expected a getter method to take no arguments, but got a var args method.",
281 element);
282 }
283
284 if (method.getReturnType() instanceof NoType) {
285 throw new ProcessingException(
286 "Expected a getter to have a return type, got void.",
287 element);
288 }
289
290 return method;
291 }
292
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500293
294 /**
295 * Determine the property type from the annotation, return type, or context clues.
296 *
Ashley Rose89d6bce2019-03-01 19:24:50 -0500297 * @param accessor An element representing the getter or field to determine the type of
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500298 * @param annotation A mirror of an inspectable property-shaped annotation
299 * @return The resolved property type
Ashley Rose0b671da2019-01-25 15:41:29 -0500300 * @throws ProcessingException If the property type cannot be resolved or is invalid
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500301 * @see android.view.inspector.InspectableProperty#valueType()
302 */
303 private Property.Type determinePropertyType(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500304 Element accessor,
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500305 AnnotationMirror annotation) {
306
307 final String valueType = mAnnotationUtils
Ashley Rose89d6bce2019-03-01 19:24:50 -0500308 .untypedValueByName("valueType", accessor, annotation)
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500309 .map(Object::toString)
310 .orElse("INFERRED");
311
Ashley Rose89d6bce2019-03-01 19:24:50 -0500312 final Property.Type accessorType =
313 convertTypeMirrorToPropertyType(extractReturnOrFieldType(accessor), accessor);
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500314
Ashley Rose0b671da2019-01-25 15:41:29 -0500315 final Optional<AnnotationValue> enumMapping =
316 mAnnotationUtils.valueByName("enumMapping", annotation);
317 final Optional<AnnotationValue> flagMapping =
318 mAnnotationUtils.valueByName("flagMapping", annotation);
319
Ashley Rose89d6bce2019-03-01 19:24:50 -0500320 if (accessorType != Property.Type.INT) {
Ashley Rose0b671da2019-01-25 15:41:29 -0500321 enumMapping.ifPresent(value -> {
322 throw new ProcessingException(
323 String.format(
324 "Can only use enumMapping on int types, got %s.",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500325 accessorType.toString().toLowerCase()),
326 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500327 annotation,
328 value);
329 });
330 flagMapping.ifPresent(value -> {
331 throw new ProcessingException(
332 String.format(
333 "Can only use flagMapping on int types, got %s.",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500334 accessorType.toString().toLowerCase()),
335 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500336 annotation,
337 value);
338 });
339 }
340
Ashley Rosee8914812019-03-05 17:12:00 -0500341
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500342 switch (valueType) {
343 case "INFERRED":
Ashley Rosee8914812019-03-05 17:12:00 -0500344 final boolean hasColor = hasColorAnnotation(accessor);
345 final boolean hasResourceId = hasResourceIdAnnotation(accessor);
346
Ashley Rose0b671da2019-01-25 15:41:29 -0500347 if (hasColor) {
348 enumMapping.ifPresent(value -> {
349 throw new ProcessingException(
350 "Cannot use enumMapping on a color type.",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500351 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500352 annotation,
353 value);
354 });
355 flagMapping.ifPresent(value -> {
356 throw new ProcessingException(
357 "Cannot use flagMapping on a color type.",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500358 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500359 annotation,
360 value);
361 });
Ashley Rosee8914812019-03-05 17:12:00 -0500362 if (hasResourceId) {
363 throw new ProcessingException(
364 "Cannot infer type, both color and resource ID annotations "
365 + "are present.",
366 accessor,
367 annotation);
368 }
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500369 return Property.Type.COLOR;
Ashley Rosee8914812019-03-05 17:12:00 -0500370 } else if (hasResourceId) {
371 enumMapping.ifPresent(value -> {
372 throw new ProcessingException(
373 "Cannot use enumMapping on a resource ID type.",
374 accessor,
375 annotation,
376 value);
377 });
378 flagMapping.ifPresent(value -> {
379 throw new ProcessingException(
380 "Cannot use flagMapping on a resource ID type.",
381 accessor,
382 annotation,
383 value);
384 });
385 return Property.Type.RESOURCE_ID;
Ashley Rose0b671da2019-01-25 15:41:29 -0500386 } else if (enumMapping.isPresent()) {
387 flagMapping.ifPresent(value -> {
388 throw new ProcessingException(
389 "Cannot use flagMapping and enumMapping simultaneously.",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500390 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500391 annotation,
392 value);
393 });
394 return Property.Type.INT_ENUM;
395 } else if (flagMapping.isPresent()) {
396 return Property.Type.INT_FLAG;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500397 } else {
Ashley Rose89d6bce2019-03-01 19:24:50 -0500398 return accessorType;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500399 }
400 case "NONE":
Ashley Rose89d6bce2019-03-01 19:24:50 -0500401 return accessorType;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500402 case "COLOR":
Ashley Rose89d6bce2019-03-01 19:24:50 -0500403 switch (accessorType) {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500404 case COLOR:
405 case INT:
406 case LONG:
407 return Property.Type.COLOR;
408 default:
409 throw new ProcessingException(
410 "Color must be a long, integer, or android.graphics.Color",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500411 accessor,
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500412 annotation);
413 }
414 case "GRAVITY":
Ashley Rose89d6bce2019-03-01 19:24:50 -0500415 requirePackedIntToBeInt("Gravity", accessorType, accessor, annotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500416 return Property.Type.GRAVITY;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500417 case "INT_ENUM":
Ashley Rose89d6bce2019-03-01 19:24:50 -0500418 requirePackedIntToBeInt("IntEnum", accessorType, accessor, annotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500419 return Property.Type.INT_ENUM;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500420 case "INT_FLAG":
Ashley Rose89d6bce2019-03-01 19:24:50 -0500421 requirePackedIntToBeInt("IntFlag", accessorType, accessor, annotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500422 return Property.Type.INT_FLAG;
Ashley Rosee8914812019-03-05 17:12:00 -0500423 case "RESOURCE_ID":
424 return Property.Type.RESOURCE_ID;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500425 default:
426 throw new ProcessingException(
427 String.format("Unknown value type enumeration value: %s", valueType),
Ashley Rose89d6bce2019-03-01 19:24:50 -0500428 accessor,
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500429 annotation);
430 }
431 }
432
433 /**
Ashley Rose89d6bce2019-03-01 19:24:50 -0500434 * Get the type of a field or the return type of a method.
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500435 *
Ashley Rose89d6bce2019-03-01 19:24:50 -0500436 * @param element The element to extract a {@link TypeMirror} from
437 * @return The return or field type of the element
438 * @throws ProcessingException If the element is not a field or a method
439 */
440 private TypeMirror extractReturnOrFieldType(Element element) {
441 switch (element.getKind()) {
442 case FIELD:
443 return element.asType();
444 case METHOD:
445 return ((ExecutableElement) element).getReturnType();
446 default:
447 throw new ProcessingException(
448 String.format(
449 "Unable to determine the type of a %s.",
450 element.getKind()),
451 element);
452 }
453 }
454
455 /**
456 * Get a property type from a type mirror
457 *
458 * @param typeMirror The type mirror to convert to a property type
459 * @param element The element to be used for exceptions
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500460 * @return The property type returned by the getter
Ashley Rose0b671da2019-01-25 15:41:29 -0500461 * @throws ProcessingException If the return type is not a primitive or an object
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500462 */
Ashley Rose89d6bce2019-03-01 19:24:50 -0500463 private Property.Type convertTypeMirrorToPropertyType(TypeMirror typeMirror, Element element) {
464 switch (unboxType(typeMirror)) {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500465 case BOOLEAN:
466 return Property.Type.BOOLEAN;
467 case BYTE:
468 return Property.Type.BYTE;
469 case CHAR:
470 return Property.Type.CHAR;
471 case DOUBLE:
472 return Property.Type.DOUBLE;
473 case FLOAT:
474 return Property.Type.FLOAT;
475 case INT:
476 return Property.Type.INT;
477 case LONG:
478 return Property.Type.LONG;
479 case SHORT:
480 return Property.Type.SHORT;
481 case DECLARED:
Ashley Rose89d6bce2019-03-01 19:24:50 -0500482 if (isColorType(typeMirror)) {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500483 return Property.Type.COLOR;
484 } else {
485 return Property.Type.OBJECT;
486 }
Ashley Roseb47ddd42019-01-28 19:54:09 -0500487 case ARRAY:
488 return Property.Type.OBJECT;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500489 default:
490 throw new ProcessingException(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500491 String.format("Unsupported property type %s.", typeMirror),
492 element);
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500493 }
494 }
495
496 /**
Ashley Rose0b671da2019-01-25 15:41:29 -0500497 * Require that a value type packed into an integer be on a getter that returns an int.
498 *
Ashley Rose89d6bce2019-03-01 19:24:50 -0500499 * @param typeName The name of the type to use in the exception
Ashley Rose0b671da2019-01-25 15:41:29 -0500500 * @param returnType The return type of the getter to check
Ashley Rose89d6bce2019-03-01 19:24:50 -0500501 * @param accessor The getter, to use in the exception
Ashley Rose0b671da2019-01-25 15:41:29 -0500502 * @param annotation The annotation, to use in the exception
503 * @throws ProcessingException If the return type is not an int
504 */
Ashley Rose89d6bce2019-03-01 19:24:50 -0500505 private static void requirePackedIntToBeInt(
Ashley Rose0b671da2019-01-25 15:41:29 -0500506 String typeName,
507 Property.Type returnType,
Ashley Rose89d6bce2019-03-01 19:24:50 -0500508 Element accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500509 AnnotationMirror annotation) {
510 if (returnType != Property.Type.INT) {
511 throw new ProcessingException(
512 String.format(
513 "%s can only be defined on a method that returns int, got %s.",
514 typeName,
515 returnType.toString().toLowerCase()),
Ashley Rose89d6bce2019-03-01 19:24:50 -0500516 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500517 annotation);
518 }
519 }
520
521 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500522 * Determine if a getter is annotated with color annotation matching its return type.
523 *
524 * Note that an {@code int} return value annotated with {@link android.annotation.ColorLong} is
525 * not considered to be annotated, nor is a {@code long} annotated with
526 * {@link android.annotation.ColorInt}.
527 *
Ashley Rose89d6bce2019-03-01 19:24:50 -0500528 * @param accessor The getter or field to query
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500529 * @return True if the getter has a color annotation, false otherwise
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500530 */
Ashley Rose89d6bce2019-03-01 19:24:50 -0500531 private boolean hasColorAnnotation(Element accessor) {
532 switch (unboxType(extractReturnOrFieldType(accessor))) {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500533 case INT:
534 for (String name : COLOR_INT_ANNOTATION_NAMES) {
Ashley Rose89d6bce2019-03-01 19:24:50 -0500535 if (mAnnotationUtils.hasAnnotation(accessor, name)) {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500536 return true;
537 }
538 }
539 return false;
540 case LONG:
541 for (String name : COLOR_LONG_ANNOTATION_NAMES) {
Ashley Rose89d6bce2019-03-01 19:24:50 -0500542 if (mAnnotationUtils.hasAnnotation(accessor, name)) {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500543 return true;
544 }
545 }
546 return false;
547 default:
548 return false;
549 }
550 }
551
552 /**
Ashley Rosee8914812019-03-05 17:12:00 -0500553 * Determine if a getter or a field is annotated with a resource ID annotation.
554 *
555 * @param accessor The getter or field to query
556 * @return True if the accessor is an integer and has a resource ID annotation, false otherwise
557 */
558 private boolean hasResourceIdAnnotation(Element accessor) {
559 if (unboxType(extractReturnOrFieldType(accessor)) == TypeKind.INT) {
560 for (String name : RESOURCE_ID_ANNOTATION_NAMES) {
561 if (mAnnotationUtils.hasAnnotation(accessor, name)) {
562 return true;
563 }
564 }
565 }
566
567 return false;
568 }
569
570 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500571 * Infer a property name from a getter method.
572 *
573 * If the method is prefixed with {@code get}, the prefix will be stripped, and the
574 * capitalization fixed. E.g.: {@code getSomeProperty} to {@code someProperty}.
575 *
576 * Additionally, if the method's return type is a boolean, an {@code is} prefix will also be
577 * stripped. E.g.: {@code isPropertyEnabled} to {@code propertyEnabled}.
578 *
579 * Failing that, this method will just return the full name of the getter.
580 *
581 * @param getter An element representing a getter
582 * @return A string property name
583 */
584 private String inferPropertyNameFromGetter(ExecutableElement getter) {
585 final String name = getter.getSimpleName().toString();
586
587 if (GETTER_GET_PREFIX.matcher(name).find()) {
588 return name.substring(3, 4).toLowerCase() + name.substring(4);
589 } else if (isBoolean(getter.getReturnType()) && GETTER_IS_PREFIX.matcher(name).find()) {
590 return name.substring(2, 3).toLowerCase() + name.substring(3);
591 } else {
592 return name;
593 }
594 }
595
596 /**
Ashley Rose0b671da2019-01-25 15:41:29 -0500597 * Build a model of an {@code int} enumeration mapping from annotation values.
598 *
599 * This method only handles the one-to-one mapping of mirrors of
600 * {@link android.view.inspector.InspectableProperty.EnumMap} annotations into
601 * {@link IntEnumEntry} objects. Further validation should be handled elsewhere
602 *
603 * @see android.view.inspector.IntEnumMapping
604 * @see android.view.inspector.InspectableProperty#enumMapping()
Ashley Rose89d6bce2019-03-01 19:24:50 -0500605 * @param accessor The accessor of the property, used for exceptions
Ashley Rose0b671da2019-01-25 15:41:29 -0500606 * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
607 * extract enum mapping values from.
608 * @return A list of int enum entries, in the order specified in source
609 * @throws ProcessingException if mapping doesn't exist or is invalid
610 */
611 private List<IntEnumEntry> processEnumMapping(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500612 Element accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500613 AnnotationMirror annotation) {
614 List<AnnotationMirror> enumAnnotations = mAnnotationUtils.typedArrayValuesByName(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500615 "enumMapping", AnnotationMirror.class, accessor, annotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500616 List<IntEnumEntry> enumEntries = new ArrayList<>(enumAnnotations.size());
617
618 if (enumAnnotations.isEmpty()) {
619 throw new ProcessingException(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500620 "Encountered an empty array for enumMapping", accessor, annotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500621 }
622
623 for (AnnotationMirror enumAnnotation : enumAnnotations) {
624 final String name = mAnnotationUtils.typedValueByName(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500625 "name", String.class, accessor, enumAnnotation)
Ashley Rose0b671da2019-01-25 15:41:29 -0500626 .orElseThrow(() -> {
627 throw new ProcessingException(
628 "Name is required for @EnumMap",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500629 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500630 enumAnnotation);
631 });
632
633 final int value = mAnnotationUtils.typedValueByName(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500634 "value", Integer.class, accessor, enumAnnotation)
Ashley Rose0b671da2019-01-25 15:41:29 -0500635 .orElseThrow(() -> {
636 throw new ProcessingException(
637 "Value is required for @EnumMap",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500638 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500639 enumAnnotation);
640 });
641
642 enumEntries.add(new IntEnumEntry(name, value));
643 }
644
645 return enumEntries;
646 }
647
648 /**
649 * Build a model of an {@code int} flag mapping from annotation values.
650 *
651 * This method only handles the one-to-one mapping of mirrors of
652 * {@link android.view.inspector.InspectableProperty.FlagMap} annotations into
653 * {@link IntFlagEntry} objects. Further validation should be handled elsewhere
654 *
655 * @see android.view.inspector.IntFlagMapping
656 * @see android.view.inspector.InspectableProperty#flagMapping()
Ashley Rose89d6bce2019-03-01 19:24:50 -0500657 * @param accessor The accessor of the property, used for exceptions
Ashley Rose0b671da2019-01-25 15:41:29 -0500658 * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
659 * extract flag mapping values from.
660 * @return A list of int flags entries, in the order specified in source
661 * @throws ProcessingException if mapping doesn't exist or is invalid
662 */
663 private List<IntFlagEntry> processFlagMapping(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500664 Element accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500665 AnnotationMirror annotation) {
666 List<AnnotationMirror> flagAnnotations = mAnnotationUtils.typedArrayValuesByName(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500667 "flagMapping", AnnotationMirror.class, accessor, annotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500668 List<IntFlagEntry> flagEntries = new ArrayList<>(flagAnnotations.size());
669
670 if (flagAnnotations.isEmpty()) {
671 throw new ProcessingException(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500672 "Encountered an empty array for flagMapping", accessor, annotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500673 }
674
675 for (AnnotationMirror flagAnnotation : flagAnnotations) {
676 final String name = mAnnotationUtils.typedValueByName(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500677 "name", String.class, accessor, flagAnnotation)
Ashley Rose0b671da2019-01-25 15:41:29 -0500678 .orElseThrow(() -> {
679 throw new ProcessingException(
680 "Name is required for @FlagMap",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500681 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500682 flagAnnotation);
683 });
684
685 final int target = mAnnotationUtils.typedValueByName(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500686 "target", Integer.class, accessor, flagAnnotation)
Ashley Rose0b671da2019-01-25 15:41:29 -0500687 .orElseThrow(() -> {
688 throw new ProcessingException(
689 "Target is required for @FlagMap",
Ashley Rose89d6bce2019-03-01 19:24:50 -0500690 accessor,
Ashley Rose0b671da2019-01-25 15:41:29 -0500691 flagAnnotation);
692 });
693
694 final Optional<Integer> mask = mAnnotationUtils.typedValueByName(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500695 "mask", Integer.class, accessor, flagAnnotation);
Ashley Rose0b671da2019-01-25 15:41:29 -0500696
697 if (mask.isPresent()) {
698 flagEntries.add(new IntFlagEntry(name, target, mask.get()));
699 } else {
700 flagEntries.add(new IntFlagEntry(name, target));
701 }
702 }
703
704 return flagEntries;
705 }
706
707 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500708 * Determine if a {@link TypeMirror} is a boxed or unboxed boolean.
709 *
710 * @param type The type mirror to check
711 * @return True if the type is a boolean
712 */
713 private boolean isBoolean(TypeMirror type) {
714 if (type.getKind() == TypeKind.DECLARED) {
715 return mProcessingEnv.getTypeUtils().unboxedType(type).getKind() == TypeKind.BOOLEAN;
716 } else {
717 return type.getKind() == TypeKind.BOOLEAN;
718 }
719 }
720
721 /**
722 * Unbox a type mirror if it represents a boxed type, otherwise pass it through.
723 *
724 * @param typeMirror The type mirror to unbox
725 * @return The same type mirror, or an unboxed primitive version
726 */
727 private TypeKind unboxType(TypeMirror typeMirror) {
728 final TypeKind typeKind = typeMirror.getKind();
729
730 if (typeKind.isPrimitive()) {
731 return typeKind;
732 } else if (typeKind == TypeKind.DECLARED) {
733 try {
734 return mProcessingEnv.getTypeUtils().unboxedType(typeMirror).getKind();
735 } catch (IllegalArgumentException e) {
736 return typeKind;
737 }
738 } else {
739 return typeKind;
740 }
741 }
742
743 /**
744 * Determine if a type mirror represents a subtype of {@link android.graphics.Color}.
745 *
746 * @param typeMirror The type mirror to test
747 * @return True if it represents a subclass of color, false otherwise
748 */
749 private boolean isColorType(TypeMirror typeMirror) {
750 final TypeElement colorType = mProcessingEnv
751 .getElementUtils()
752 .getTypeElement("android.graphics.Color");
753
754 if (colorType == null) {
755 return false;
756 } else {
757 return mProcessingEnv.getTypeUtils().isSubtype(typeMirror, colorType.asType());
758 }
759 }
760}