Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 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 | |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 19 | import android.processor.view.inspector.InspectableClassModel.Accessor; |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 20 | import android.processor.view.inspector.InspectableClassModel.IntEnumEntry; |
| 21 | import android.processor.view.inspector.InspectableClassModel.IntFlagEntry; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 22 | import android.processor.view.inspector.InspectableClassModel.Property; |
| 23 | |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 24 | import java.util.ArrayList; |
| 25 | import java.util.List; |
| 26 | import java.util.Optional; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 27 | import java.util.Set; |
| 28 | import java.util.regex.Pattern; |
| 29 | |
| 30 | import javax.annotation.processing.ProcessingEnvironment; |
| 31 | import javax.lang.model.element.AnnotationMirror; |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 32 | import javax.lang.model.element.AnnotationValue; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 33 | import javax.lang.model.element.Element; |
| 34 | import javax.lang.model.element.ElementKind; |
| 35 | import javax.lang.model.element.ExecutableElement; |
| 36 | import javax.lang.model.element.Modifier; |
| 37 | import javax.lang.model.element.TypeElement; |
| 38 | import javax.lang.model.type.NoType; |
| 39 | import javax.lang.model.type.TypeKind; |
| 40 | import javax.lang.model.type.TypeMirror; |
| 41 | |
| 42 | /** |
| 43 | * Process {@code @InspectableProperty} annotations. |
| 44 | * |
| 45 | * @see android.view.inspector.InspectableProperty |
| 46 | */ |
| 47 | public 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 Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 73 | * |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 74 | * @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 Rose | e891481 | 2019-03-05 17:12:00 -0500 | [diff] [blame] | 81 | * 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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 129 | * @param annotationQualifiedName The qualified name of the annotation to process |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 130 | * @param processingEnv The processing environment from the parent processor |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 131 | */ |
| 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 145 | final Property property = buildProperty(element, annotation); |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 146 | |
| 147 | model.getProperty(property.getName()).ifPresent(p -> { |
| 148 | throw new ProcessingException( |
| 149 | String.format( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 150 | "Property \"%s\" is already defined on #%s.", |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 151 | p.getName(), |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 152 | p.getAccessor().invocation()), |
| 153 | element, |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 154 | annotation); |
| 155 | }); |
| 156 | |
| 157 | model.putProperty(property); |
| 158 | } catch (ProcessingException processingException) { |
| 159 | processingException.print(mProcessingEnv.getMessager()); |
| 160 | } |
| 161 | } |
| 162 | |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 163 | |
| 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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 252 | /** |
| 253 | * Check that an element is shaped like a getter. |
| 254 | * |
| 255 | * @param element An element that hopefully represents a getter |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 256 | * @return An {@link ExecutableElement} that represents a getter method. |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 257 | * @throws ProcessingException if the element isn't a getter |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 258 | */ |
| 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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 267 | |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 268 | |
| 269 | if (!method.getParameters().isEmpty()) { |
| 270 | throw new ProcessingException( |
| 271 | String.format( |
| 272 | "Expected a getter method to take no parameters, " |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 273 | + "but got %d parameters.", |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 274 | 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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 293 | |
| 294 | /** |
| 295 | * Determine the property type from the annotation, return type, or context clues. |
| 296 | * |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 297 | * @param accessor An element representing the getter or field to determine the type of |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 298 | * @param annotation A mirror of an inspectable property-shaped annotation |
| 299 | * @return The resolved property type |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 300 | * @throws ProcessingException If the property type cannot be resolved or is invalid |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 301 | * @see android.view.inspector.InspectableProperty#valueType() |
| 302 | */ |
| 303 | private Property.Type determinePropertyType( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 304 | Element accessor, |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 305 | AnnotationMirror annotation) { |
| 306 | |
| 307 | final String valueType = mAnnotationUtils |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 308 | .untypedValueByName("valueType", accessor, annotation) |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 309 | .map(Object::toString) |
| 310 | .orElse("INFERRED"); |
| 311 | |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 312 | final Property.Type accessorType = |
| 313 | convertTypeMirrorToPropertyType(extractReturnOrFieldType(accessor), accessor); |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 314 | |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 315 | final Optional<AnnotationValue> enumMapping = |
| 316 | mAnnotationUtils.valueByName("enumMapping", annotation); |
| 317 | final Optional<AnnotationValue> flagMapping = |
| 318 | mAnnotationUtils.valueByName("flagMapping", annotation); |
| 319 | |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 320 | if (accessorType != Property.Type.INT) { |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 321 | enumMapping.ifPresent(value -> { |
| 322 | throw new ProcessingException( |
| 323 | String.format( |
| 324 | "Can only use enumMapping on int types, got %s.", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 325 | accessorType.toString().toLowerCase()), |
| 326 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 327 | 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 334 | accessorType.toString().toLowerCase()), |
| 335 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 336 | annotation, |
| 337 | value); |
| 338 | }); |
| 339 | } |
| 340 | |
Ashley Rose | e891481 | 2019-03-05 17:12:00 -0500 | [diff] [blame] | 341 | |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 342 | switch (valueType) { |
| 343 | case "INFERRED": |
Ashley Rose | e891481 | 2019-03-05 17:12:00 -0500 | [diff] [blame] | 344 | final boolean hasColor = hasColorAnnotation(accessor); |
| 345 | final boolean hasResourceId = hasResourceIdAnnotation(accessor); |
| 346 | |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 347 | if (hasColor) { |
| 348 | enumMapping.ifPresent(value -> { |
| 349 | throw new ProcessingException( |
| 350 | "Cannot use enumMapping on a color type.", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 351 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 352 | annotation, |
| 353 | value); |
| 354 | }); |
| 355 | flagMapping.ifPresent(value -> { |
| 356 | throw new ProcessingException( |
| 357 | "Cannot use flagMapping on a color type.", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 358 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 359 | annotation, |
| 360 | value); |
| 361 | }); |
Ashley Rose | e891481 | 2019-03-05 17:12:00 -0500 | [diff] [blame] | 362 | 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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 369 | return Property.Type.COLOR; |
Ashley Rose | e891481 | 2019-03-05 17:12:00 -0500 | [diff] [blame] | 370 | } 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 Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 386 | } else if (enumMapping.isPresent()) { |
| 387 | flagMapping.ifPresent(value -> { |
| 388 | throw new ProcessingException( |
| 389 | "Cannot use flagMapping and enumMapping simultaneously.", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 390 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 391 | annotation, |
| 392 | value); |
| 393 | }); |
| 394 | return Property.Type.INT_ENUM; |
| 395 | } else if (flagMapping.isPresent()) { |
| 396 | return Property.Type.INT_FLAG; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 397 | } else { |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 398 | return accessorType; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 399 | } |
| 400 | case "NONE": |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 401 | return accessorType; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 402 | case "COLOR": |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 403 | switch (accessorType) { |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 404 | 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 411 | accessor, |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 412 | annotation); |
| 413 | } |
| 414 | case "GRAVITY": |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 415 | requirePackedIntToBeInt("Gravity", accessorType, accessor, annotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 416 | return Property.Type.GRAVITY; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 417 | case "INT_ENUM": |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 418 | requirePackedIntToBeInt("IntEnum", accessorType, accessor, annotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 419 | return Property.Type.INT_ENUM; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 420 | case "INT_FLAG": |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 421 | requirePackedIntToBeInt("IntFlag", accessorType, accessor, annotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 422 | return Property.Type.INT_FLAG; |
Ashley Rose | e891481 | 2019-03-05 17:12:00 -0500 | [diff] [blame] | 423 | case "RESOURCE_ID": |
| 424 | return Property.Type.RESOURCE_ID; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 425 | default: |
| 426 | throw new ProcessingException( |
| 427 | String.format("Unknown value type enumeration value: %s", valueType), |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 428 | accessor, |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 429 | annotation); |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | /** |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 434 | * Get the type of a field or the return type of a method. |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 435 | * |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 436 | * @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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 460 | * @return The property type returned by the getter |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 461 | * @throws ProcessingException If the return type is not a primitive or an object |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 462 | */ |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 463 | private Property.Type convertTypeMirrorToPropertyType(TypeMirror typeMirror, Element element) { |
| 464 | switch (unboxType(typeMirror)) { |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 465 | 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 482 | if (isColorType(typeMirror)) { |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 483 | return Property.Type.COLOR; |
| 484 | } else { |
| 485 | return Property.Type.OBJECT; |
| 486 | } |
Ashley Rose | b47ddd4 | 2019-01-28 19:54:09 -0500 | [diff] [blame] | 487 | case ARRAY: |
| 488 | return Property.Type.OBJECT; |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 489 | default: |
| 490 | throw new ProcessingException( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 491 | String.format("Unsupported property type %s.", typeMirror), |
| 492 | element); |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 493 | } |
| 494 | } |
| 495 | |
| 496 | /** |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 497 | * Require that a value type packed into an integer be on a getter that returns an int. |
| 498 | * |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 499 | * @param typeName The name of the type to use in the exception |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 500 | * @param returnType The return type of the getter to check |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 501 | * @param accessor The getter, to use in the exception |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 502 | * @param annotation The annotation, to use in the exception |
| 503 | * @throws ProcessingException If the return type is not an int |
| 504 | */ |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 505 | private static void requirePackedIntToBeInt( |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 506 | String typeName, |
| 507 | Property.Type returnType, |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 508 | Element accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 509 | 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 516 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 517 | annotation); |
| 518 | } |
| 519 | } |
| 520 | |
| 521 | /** |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 522 | * 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 528 | * @param accessor The getter or field to query |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 529 | * @return True if the getter has a color annotation, false otherwise |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 530 | */ |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 531 | private boolean hasColorAnnotation(Element accessor) { |
| 532 | switch (unboxType(extractReturnOrFieldType(accessor))) { |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 533 | case INT: |
| 534 | for (String name : COLOR_INT_ANNOTATION_NAMES) { |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 535 | if (mAnnotationUtils.hasAnnotation(accessor, name)) { |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 536 | return true; |
| 537 | } |
| 538 | } |
| 539 | return false; |
| 540 | case LONG: |
| 541 | for (String name : COLOR_LONG_ANNOTATION_NAMES) { |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 542 | if (mAnnotationUtils.hasAnnotation(accessor, name)) { |
Ashley Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 543 | return true; |
| 544 | } |
| 545 | } |
| 546 | return false; |
| 547 | default: |
| 548 | return false; |
| 549 | } |
| 550 | } |
| 551 | |
| 552 | /** |
Ashley Rose | e891481 | 2019-03-05 17:12:00 -0500 | [diff] [blame] | 553 | * 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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 571 | * 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 Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 597 | * 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 605 | * @param accessor The accessor of the property, used for exceptions |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 606 | * @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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 612 | Element accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 613 | AnnotationMirror annotation) { |
| 614 | List<AnnotationMirror> enumAnnotations = mAnnotationUtils.typedArrayValuesByName( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 615 | "enumMapping", AnnotationMirror.class, accessor, annotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 616 | List<IntEnumEntry> enumEntries = new ArrayList<>(enumAnnotations.size()); |
| 617 | |
| 618 | if (enumAnnotations.isEmpty()) { |
| 619 | throw new ProcessingException( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 620 | "Encountered an empty array for enumMapping", accessor, annotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 621 | } |
| 622 | |
| 623 | for (AnnotationMirror enumAnnotation : enumAnnotations) { |
| 624 | final String name = mAnnotationUtils.typedValueByName( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 625 | "name", String.class, accessor, enumAnnotation) |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 626 | .orElseThrow(() -> { |
| 627 | throw new ProcessingException( |
| 628 | "Name is required for @EnumMap", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 629 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 630 | enumAnnotation); |
| 631 | }); |
| 632 | |
| 633 | final int value = mAnnotationUtils.typedValueByName( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 634 | "value", Integer.class, accessor, enumAnnotation) |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 635 | .orElseThrow(() -> { |
| 636 | throw new ProcessingException( |
| 637 | "Value is required for @EnumMap", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 638 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 639 | 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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 657 | * @param accessor The accessor of the property, used for exceptions |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 658 | * @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 Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 664 | Element accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 665 | AnnotationMirror annotation) { |
| 666 | List<AnnotationMirror> flagAnnotations = mAnnotationUtils.typedArrayValuesByName( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 667 | "flagMapping", AnnotationMirror.class, accessor, annotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 668 | List<IntFlagEntry> flagEntries = new ArrayList<>(flagAnnotations.size()); |
| 669 | |
| 670 | if (flagAnnotations.isEmpty()) { |
| 671 | throw new ProcessingException( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 672 | "Encountered an empty array for flagMapping", accessor, annotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 673 | } |
| 674 | |
| 675 | for (AnnotationMirror flagAnnotation : flagAnnotations) { |
| 676 | final String name = mAnnotationUtils.typedValueByName( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 677 | "name", String.class, accessor, flagAnnotation) |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 678 | .orElseThrow(() -> { |
| 679 | throw new ProcessingException( |
| 680 | "Name is required for @FlagMap", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 681 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 682 | flagAnnotation); |
| 683 | }); |
| 684 | |
| 685 | final int target = mAnnotationUtils.typedValueByName( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 686 | "target", Integer.class, accessor, flagAnnotation) |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 687 | .orElseThrow(() -> { |
| 688 | throw new ProcessingException( |
| 689 | "Target is required for @FlagMap", |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 690 | accessor, |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 691 | flagAnnotation); |
| 692 | }); |
| 693 | |
| 694 | final Optional<Integer> mask = mAnnotationUtils.typedValueByName( |
Ashley Rose | 89d6bce | 2019-03-01 19:24:50 -0500 | [diff] [blame] | 695 | "mask", Integer.class, accessor, flagAnnotation); |
Ashley Rose | 0b671da | 2019-01-25 15:41:29 -0500 | [diff] [blame] | 696 | |
| 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 Rose | c1a4dec | 2018-12-13 18:06:30 -0500 | [diff] [blame] | 708 | * 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 | } |