blob: 50c79da25eba0f19ce28afb868352645db330e1d [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 Rose0b671da2019-01-25 15:41:29 -050019import android.processor.view.inspector.InspectableClassModel.IntEnumEntry;
20import android.processor.view.inspector.InspectableClassModel.IntFlagEntry;
Ashley Rosec1a4dec2018-12-13 18:06:30 -050021import android.processor.view.inspector.InspectableClassModel.Property;
22
Ashley Rose0b671da2019-01-25 15:41:29 -050023import java.util.ArrayList;
24import java.util.List;
25import java.util.Optional;
Ashley Rosec1a4dec2018-12-13 18:06:30 -050026import java.util.Set;
27import java.util.regex.Pattern;
28
29import javax.annotation.processing.ProcessingEnvironment;
30import javax.lang.model.element.AnnotationMirror;
Ashley Rose0b671da2019-01-25 15:41:29 -050031import javax.lang.model.element.AnnotationValue;
Ashley Rosec1a4dec2018-12-13 18:06:30 -050032import javax.lang.model.element.Element;
33import javax.lang.model.element.ElementKind;
34import javax.lang.model.element.ExecutableElement;
35import javax.lang.model.element.Modifier;
36import javax.lang.model.element.TypeElement;
37import javax.lang.model.type.NoType;
38import javax.lang.model.type.TypeKind;
39import javax.lang.model.type.TypeMirror;
40
41/**
42 * Process {@code @InspectableProperty} annotations.
43 *
44 * @see android.view.inspector.InspectableProperty
45 */
46public final class InspectablePropertyProcessor implements ModelProcessor {
47 private final String mQualifiedName;
48 private final ProcessingEnvironment mProcessingEnv;
49 private final AnnotationUtils mAnnotationUtils;
50
51 /**
52 * Regex that matches methods names of the form {@code #getValue()}.
53 */
54 private static final Pattern GETTER_GET_PREFIX = Pattern.compile("\\Aget[A-Z]");
55
56 /**
57 * Regex that matches method name of the form {@code #isPredicate()}.
58 */
59 private static final Pattern GETTER_IS_PREFIX = Pattern.compile("\\Ais[A-Z]");
60
61 /**
62 * Set of android and androidx annotation qualified names for colors packed into {@code int}.
63 *
64 * @see android.annotation.ColorInt
65 */
66 private static final String[] COLOR_INT_ANNOTATION_NAMES = {
67 "android.annotation.ColorInt",
68 "androidx.annotation.ColorInt"};
69
70 /**
71 * Set of android and androidx annotation qualified names for colors packed into {@code long}.
Ashley Rose0b671da2019-01-25 15:41:29 -050072 *
Ashley Rosec1a4dec2018-12-13 18:06:30 -050073 * @see android.annotation.ColorLong
74 */
75 private static final String[] COLOR_LONG_ANNOTATION_NAMES = {
76 "android.annotation.ColorLong",
77 "androidx.annotation.ColorLong"};
78
79 /**
80 * @param annotationQualifiedName The qualified name of the annotation to process
Ashley Rose0b671da2019-01-25 15:41:29 -050081 * @param processingEnv The processing environment from the parent processor
Ashley Rosec1a4dec2018-12-13 18:06:30 -050082 */
83 public InspectablePropertyProcessor(
84 String annotationQualifiedName,
85 ProcessingEnvironment processingEnv) {
86 mQualifiedName = annotationQualifiedName;
87 mProcessingEnv = processingEnv;
88 mAnnotationUtils = new AnnotationUtils(processingEnv);
89 }
90
91 @Override
92 public void process(Element element, InspectableClassModel model) {
93 try {
94 final AnnotationMirror annotation =
95 mAnnotationUtils.exactlyOneMirror(mQualifiedName, element);
96 final ExecutableElement getter = ensureGetter(element);
97 final Property property = buildProperty(getter, annotation);
98
99 model.getProperty(property.getName()).ifPresent(p -> {
100 throw new ProcessingException(
101 String.format(
102 "Property \"%s\" is already defined on #%s().",
103 p.getName(),
104 p.getGetter()),
105 getter,
106 annotation);
107 });
108
109 model.putProperty(property);
110 } catch (ProcessingException processingException) {
111 processingException.print(mProcessingEnv.getMessager());
112 }
113 }
114
115 /**
116 * Check that an element is shaped like a getter.
117 *
118 * @param element An element that hopefully represents a getter
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500119 * @return An {@link ExecutableElement} that represents a getter method.
Ashley Rose0b671da2019-01-25 15:41:29 -0500120 * @throws ProcessingException if the element isn't a getter
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500121 */
122 private ExecutableElement ensureGetter(Element element) {
123 if (element.getKind() != ElementKind.METHOD) {
124 throw new ProcessingException(
125 String.format("Expected a method, got a %s", element.getKind()),
126 element);
127 }
128
129 final ExecutableElement method = (ExecutableElement) element;
130 final Set<Modifier> modifiers = method.getModifiers();
131
132 if (modifiers.contains(Modifier.PRIVATE)) {
133 throw new ProcessingException(
134 "Property getter methods must not be private.",
135 element);
136 }
137
138 if (modifiers.contains(Modifier.ABSTRACT)) {
139 throw new ProcessingException(
140 "Property getter methods must not be abstract.",
141 element);
142 }
143
144 if (modifiers.contains(Modifier.STATIC)) {
145 throw new ProcessingException(
146 "Property getter methods must not be static.",
147 element);
148 }
149
150 if (!method.getParameters().isEmpty()) {
151 throw new ProcessingException(
152 String.format(
153 "Expected a getter method to take no parameters, "
Ashley Rose0b671da2019-01-25 15:41:29 -0500154 + "but got %d parameters.",
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500155 method.getParameters().size()),
156 element);
157 }
158
159 if (method.isVarArgs()) {
160 throw new ProcessingException(
161 "Expected a getter method to take no arguments, but got a var args method.",
162 element);
163 }
164
165 if (method.getReturnType() instanceof NoType) {
166 throw new ProcessingException(
167 "Expected a getter to have a return type, got void.",
168 element);
169 }
170
171 return method;
172 }
173
174 /**
175 * Build a {@link Property} from a getter and an inspectable property annotation.
176 *
Ashley Rose0b671da2019-01-25 15:41:29 -0500177 * @param getter An element representing the getter to build from
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500178 * @param annotation A mirror of an inspectable property-shaped annotation
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500179 * @return A property for the getter and annotation
Ashley Rose0b671da2019-01-25 15:41:29 -0500180 * @throws ProcessingException If the supplied data is invalid and a property cannot be modeled
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500181 */
182 private Property buildProperty(ExecutableElement getter, AnnotationMirror annotation) {
183 final String name = mAnnotationUtils
184 .typedValueByName("name", String.class, getter, annotation)
185 .orElseGet(() -> inferPropertyNameFromGetter(getter));
186
187 final Property property = new Property(
188 name,
189 getter.getSimpleName().toString(),
190 determinePropertyType(getter, annotation));
191
192 mAnnotationUtils
193 .typedValueByName("hasAttributeId", Boolean.class, getter, annotation)
194 .ifPresent(property::setAttributeIdInferrableFromR);
195
196 mAnnotationUtils
197 .typedValueByName("attributeId", Integer.class, getter, annotation)
198 .ifPresent(property::setAttributeId);
199
Ashley Rose0b671da2019-01-25 15:41:29 -0500200 switch (property.getType()) {
201 case INT_ENUM:
202 property.setIntEnumEntries(processEnumMapping(getter, annotation));
203 break;
204 case INT_FLAG:
205 property.setIntFlagEntries(processFlagMapping(getter, annotation));
206 break;
207 }
208
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500209 return property;
210 }
211
212 /**
213 * Determine the property type from the annotation, return type, or context clues.
214 *
Ashley Rose0b671da2019-01-25 15:41:29 -0500215 * @param getter An element representing the getter to build from
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500216 * @param annotation A mirror of an inspectable property-shaped annotation
217 * @return The resolved property type
Ashley Rose0b671da2019-01-25 15:41:29 -0500218 * @throws ProcessingException If the property type cannot be resolved or is invalid
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500219 * @see android.view.inspector.InspectableProperty#valueType()
220 */
221 private Property.Type determinePropertyType(
222 ExecutableElement getter,
223 AnnotationMirror annotation) {
224
225 final String valueType = mAnnotationUtils
226 .untypedValueByName("valueType", getter, annotation)
227 .map(Object::toString)
228 .orElse("INFERRED");
229
230 final Property.Type returnType = convertReturnTypeToPropertyType(getter);
231
Ashley Rose0b671da2019-01-25 15:41:29 -0500232 final boolean hasColor = hasColorAnnotation(getter);
233 final Optional<AnnotationValue> enumMapping =
234 mAnnotationUtils.valueByName("enumMapping", annotation);
235 final Optional<AnnotationValue> flagMapping =
236 mAnnotationUtils.valueByName("flagMapping", annotation);
237
238 if (returnType != Property.Type.INT) {
239 enumMapping.ifPresent(value -> {
240 throw new ProcessingException(
241 String.format(
242 "Can only use enumMapping on int types, got %s.",
243 returnType.toString().toLowerCase()),
244 getter,
245 annotation,
246 value);
247 });
248 flagMapping.ifPresent(value -> {
249 throw new ProcessingException(
250 String.format(
251 "Can only use flagMapping on int types, got %s.",
252 returnType.toString().toLowerCase()),
253 getter,
254 annotation,
255 value);
256 });
257 }
258
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500259 switch (valueType) {
260 case "INFERRED":
Ashley Rose0b671da2019-01-25 15:41:29 -0500261 if (hasColor) {
262 enumMapping.ifPresent(value -> {
263 throw new ProcessingException(
264 "Cannot use enumMapping on a color type.",
265 getter,
266 annotation,
267 value);
268 });
269 flagMapping.ifPresent(value -> {
270 throw new ProcessingException(
271 "Cannot use flagMapping on a color type.",
272 getter,
273 annotation,
274 value);
275 });
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500276 return Property.Type.COLOR;
Ashley Rose0b671da2019-01-25 15:41:29 -0500277 } else if (enumMapping.isPresent()) {
278 flagMapping.ifPresent(value -> {
279 throw new ProcessingException(
280 "Cannot use flagMapping and enumMapping simultaneously.",
281 getter,
282 annotation,
283 value);
284 });
285 return Property.Type.INT_ENUM;
286 } else if (flagMapping.isPresent()) {
287 return Property.Type.INT_FLAG;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500288 } else {
289 return returnType;
290 }
291 case "NONE":
292 return returnType;
293 case "COLOR":
294 switch (returnType) {
295 case COLOR:
296 case INT:
297 case LONG:
298 return Property.Type.COLOR;
299 default:
300 throw new ProcessingException(
301 "Color must be a long, integer, or android.graphics.Color",
302 getter,
303 annotation);
304 }
305 case "GRAVITY":
Ashley Rose0b671da2019-01-25 15:41:29 -0500306 requirePackedIntToReturnInt("Gravity", returnType, getter, annotation);
307 return Property.Type.GRAVITY;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500308 case "INT_ENUM":
Ashley Rose0b671da2019-01-25 15:41:29 -0500309 requirePackedIntToReturnInt("IntEnum", returnType, getter, annotation);
310 return Property.Type.INT_ENUM;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500311 case "INT_FLAG":
Ashley Rose0b671da2019-01-25 15:41:29 -0500312 requirePackedIntToReturnInt("IntFlag", returnType, getter, annotation);
313 return Property.Type.INT_FLAG;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500314 default:
315 throw new ProcessingException(
316 String.format("Unknown value type enumeration value: %s", valueType),
317 getter,
318 annotation);
319 }
320 }
321
322 /**
323 * Get a property type from the return type of a getter.
324 *
325 * @param getter The getter to extract the return type of
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500326 * @return The property type returned by the getter
Ashley Rose0b671da2019-01-25 15:41:29 -0500327 * @throws ProcessingException If the return type is not a primitive or an object
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500328 */
329 private Property.Type convertReturnTypeToPropertyType(ExecutableElement getter) {
330 final TypeMirror returnType = getter.getReturnType();
331
332 switch (unboxType(returnType)) {
333 case BOOLEAN:
334 return Property.Type.BOOLEAN;
335 case BYTE:
336 return Property.Type.BYTE;
337 case CHAR:
338 return Property.Type.CHAR;
339 case DOUBLE:
340 return Property.Type.DOUBLE;
341 case FLOAT:
342 return Property.Type.FLOAT;
343 case INT:
344 return Property.Type.INT;
345 case LONG:
346 return Property.Type.LONG;
347 case SHORT:
348 return Property.Type.SHORT;
349 case DECLARED:
350 if (isColorType(returnType)) {
351 return Property.Type.COLOR;
352 } else {
353 return Property.Type.OBJECT;
354 }
Ashley Roseb47ddd42019-01-28 19:54:09 -0500355 case ARRAY:
356 return Property.Type.OBJECT;
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500357 default:
358 throw new ProcessingException(
359 String.format("Unsupported return type %s.", returnType),
360 getter);
361 }
362 }
363
364 /**
Ashley Rose0b671da2019-01-25 15:41:29 -0500365 * Require that a value type packed into an integer be on a getter that returns an int.
366 *
367 * @param typeName The name of the type to use in the exception
368 * @param returnType The return type of the getter to check
369 * @param getter The getter, to use in the exception
370 * @param annotation The annotation, to use in the exception
371 * @throws ProcessingException If the return type is not an int
372 */
373 private static void requirePackedIntToReturnInt(
374 String typeName,
375 Property.Type returnType,
376 ExecutableElement getter,
377 AnnotationMirror annotation) {
378 if (returnType != Property.Type.INT) {
379 throw new ProcessingException(
380 String.format(
381 "%s can only be defined on a method that returns int, got %s.",
382 typeName,
383 returnType.toString().toLowerCase()),
384 getter,
385 annotation);
386 }
387 }
388
389 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500390 * Determine if a getter is annotated with color annotation matching its return type.
391 *
392 * Note that an {@code int} return value annotated with {@link android.annotation.ColorLong} is
393 * not considered to be annotated, nor is a {@code long} annotated with
394 * {@link android.annotation.ColorInt}.
395 *
396 * @param getter The getter to query
397 * @return True if the getter has a color annotation, false otherwise
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500398 */
399 private boolean hasColorAnnotation(ExecutableElement getter) {
400 switch (unboxType(getter.getReturnType())) {
401 case INT:
402 for (String name : COLOR_INT_ANNOTATION_NAMES) {
403 if (mAnnotationUtils.hasAnnotation(getter, name)) {
404 return true;
405 }
406 }
407 return false;
408 case LONG:
409 for (String name : COLOR_LONG_ANNOTATION_NAMES) {
410 if (mAnnotationUtils.hasAnnotation(getter, name)) {
411 return true;
412 }
413 }
414 return false;
415 default:
416 return false;
417 }
418 }
419
420 /**
421 * Infer a property name from a getter method.
422 *
423 * If the method is prefixed with {@code get}, the prefix will be stripped, and the
424 * capitalization fixed. E.g.: {@code getSomeProperty} to {@code someProperty}.
425 *
426 * Additionally, if the method's return type is a boolean, an {@code is} prefix will also be
427 * stripped. E.g.: {@code isPropertyEnabled} to {@code propertyEnabled}.
428 *
429 * Failing that, this method will just return the full name of the getter.
430 *
431 * @param getter An element representing a getter
432 * @return A string property name
433 */
434 private String inferPropertyNameFromGetter(ExecutableElement getter) {
435 final String name = getter.getSimpleName().toString();
436
437 if (GETTER_GET_PREFIX.matcher(name).find()) {
438 return name.substring(3, 4).toLowerCase() + name.substring(4);
439 } else if (isBoolean(getter.getReturnType()) && GETTER_IS_PREFIX.matcher(name).find()) {
440 return name.substring(2, 3).toLowerCase() + name.substring(3);
441 } else {
442 return name;
443 }
444 }
445
446 /**
Ashley Rose0b671da2019-01-25 15:41:29 -0500447 * Build a model of an {@code int} enumeration mapping from annotation values.
448 *
449 * This method only handles the one-to-one mapping of mirrors of
450 * {@link android.view.inspector.InspectableProperty.EnumMap} annotations into
451 * {@link IntEnumEntry} objects. Further validation should be handled elsewhere
452 *
453 * @see android.view.inspector.IntEnumMapping
454 * @see android.view.inspector.InspectableProperty#enumMapping()
455 * @param getter The getter of the property, used for exceptions
456 * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
457 * extract enum mapping values from.
458 * @return A list of int enum entries, in the order specified in source
459 * @throws ProcessingException if mapping doesn't exist or is invalid
460 */
461 private List<IntEnumEntry> processEnumMapping(
462 ExecutableElement getter,
463 AnnotationMirror annotation) {
464 List<AnnotationMirror> enumAnnotations = mAnnotationUtils.typedArrayValuesByName(
465 "enumMapping", AnnotationMirror.class, getter, annotation);
466 List<IntEnumEntry> enumEntries = new ArrayList<>(enumAnnotations.size());
467
468 if (enumAnnotations.isEmpty()) {
469 throw new ProcessingException(
470 "Encountered an empty array for enumMapping", getter, annotation);
471 }
472
473 for (AnnotationMirror enumAnnotation : enumAnnotations) {
474 final String name = mAnnotationUtils.typedValueByName(
475 "name", String.class, getter, enumAnnotation)
476 .orElseThrow(() -> {
477 throw new ProcessingException(
478 "Name is required for @EnumMap",
479 getter,
480 enumAnnotation);
481 });
482
483 final int value = mAnnotationUtils.typedValueByName(
484 "value", Integer.class, getter, enumAnnotation)
485 .orElseThrow(() -> {
486 throw new ProcessingException(
487 "Value is required for @EnumMap",
488 getter,
489 enumAnnotation);
490 });
491
492 enumEntries.add(new IntEnumEntry(name, value));
493 }
494
495 return enumEntries;
496 }
497
498 /**
499 * Build a model of an {@code int} flag mapping from annotation values.
500 *
501 * This method only handles the one-to-one mapping of mirrors of
502 * {@link android.view.inspector.InspectableProperty.FlagMap} annotations into
503 * {@link IntFlagEntry} objects. Further validation should be handled elsewhere
504 *
505 * @see android.view.inspector.IntFlagMapping
506 * @see android.view.inspector.InspectableProperty#flagMapping()
507 * @param getter The getter of the property, used for exceptions
508 * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to
509 * extract flag mapping values from.
510 * @return A list of int flags entries, in the order specified in source
511 * @throws ProcessingException if mapping doesn't exist or is invalid
512 */
513 private List<IntFlagEntry> processFlagMapping(
514 ExecutableElement getter,
515 AnnotationMirror annotation) {
516 List<AnnotationMirror> flagAnnotations = mAnnotationUtils.typedArrayValuesByName(
517 "flagMapping", AnnotationMirror.class, getter, annotation);
518 List<IntFlagEntry> flagEntries = new ArrayList<>(flagAnnotations.size());
519
520 if (flagAnnotations.isEmpty()) {
521 throw new ProcessingException(
522 "Encountered an empty array for flagMapping", getter, annotation);
523 }
524
525 for (AnnotationMirror flagAnnotation : flagAnnotations) {
526 final String name = mAnnotationUtils.typedValueByName(
527 "name", String.class, getter, flagAnnotation)
528 .orElseThrow(() -> {
529 throw new ProcessingException(
530 "Name is required for @FlagMap",
531 getter,
532 flagAnnotation);
533 });
534
535 final int target = mAnnotationUtils.typedValueByName(
536 "target", Integer.class, getter, flagAnnotation)
537 .orElseThrow(() -> {
538 throw new ProcessingException(
539 "Target is required for @FlagMap",
540 getter,
541 flagAnnotation);
542 });
543
544 final Optional<Integer> mask = mAnnotationUtils.typedValueByName(
545 "mask", Integer.class, getter, flagAnnotation);
546
547 if (mask.isPresent()) {
548 flagEntries.add(new IntFlagEntry(name, target, mask.get()));
549 } else {
550 flagEntries.add(new IntFlagEntry(name, target));
551 }
552 }
553
554 return flagEntries;
555 }
556
557 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500558 * Determine if a {@link TypeMirror} is a boxed or unboxed boolean.
559 *
560 * @param type The type mirror to check
561 * @return True if the type is a boolean
562 */
563 private boolean isBoolean(TypeMirror type) {
564 if (type.getKind() == TypeKind.DECLARED) {
565 return mProcessingEnv.getTypeUtils().unboxedType(type).getKind() == TypeKind.BOOLEAN;
566 } else {
567 return type.getKind() == TypeKind.BOOLEAN;
568 }
569 }
570
571 /**
572 * Unbox a type mirror if it represents a boxed type, otherwise pass it through.
573 *
574 * @param typeMirror The type mirror to unbox
575 * @return The same type mirror, or an unboxed primitive version
576 */
577 private TypeKind unboxType(TypeMirror typeMirror) {
578 final TypeKind typeKind = typeMirror.getKind();
579
580 if (typeKind.isPrimitive()) {
581 return typeKind;
582 } else if (typeKind == TypeKind.DECLARED) {
583 try {
584 return mProcessingEnv.getTypeUtils().unboxedType(typeMirror).getKind();
585 } catch (IllegalArgumentException e) {
586 return typeKind;
587 }
588 } else {
589 return typeKind;
590 }
591 }
592
593 /**
594 * Determine if a type mirror represents a subtype of {@link android.graphics.Color}.
595 *
596 * @param typeMirror The type mirror to test
597 * @return True if it represents a subclass of color, false otherwise
598 */
599 private boolean isColorType(TypeMirror typeMirror) {
600 final TypeElement colorType = mProcessingEnv
601 .getElementUtils()
602 .getTypeElement("android.graphics.Color");
603
604 if (colorType == null) {
605 return false;
606 } else {
607 return mProcessingEnv.getTypeUtils().isSubtype(typeMirror, colorType.asType());
608 }
609 }
610}