1 /* 2 * Copyright (C) 2022 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.car.test.util; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import android.car.annotation.AddedInOrBefore; 22 23 import java.lang.annotation.Annotation; 24 import java.lang.reflect.Field; 25 import java.lang.reflect.Method; 26 import java.lang.reflect.Modifier; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.HashSet; 30 import java.util.List; 31 32 33 // TODO(b/237565347): Refactor this class so that 'field' and 'method' code is not repeated. 34 /** 35 * Helper for evaluating annotations in tests. 36 */ 37 public class AnnotationHelper { 38 39 public static final HashSet<String> sJavaLangObjectNames; 40 41 static { 42 Method[] objectMethods = Object.class.getMethods(); 43 sJavaLangObjectNames = new HashSet<>(objectMethods.length); 44 45 for (Method method : objectMethods) { 46 sJavaLangObjectNames.add( 47 method.getReturnType().toString() + method.getName() + Arrays.toString( 48 method.getParameterTypes())); 49 } 50 51 // getMethods excludes protected functions. 52 sJavaLangObjectNames.add("voidfinalize[]"); 53 } 54 checkForAnnotation(String[] classes, HashSet<String> addedInOrBeforeApis, Class<?>... annotationClasses)55 public static void checkForAnnotation(String[] classes, HashSet<String> addedInOrBeforeApis, 56 Class<?>... annotationClasses) 57 throws Exception { 58 List<String> errorsNoAnnotation = new ArrayList<>(); 59 List<String> errorsExtraAnnotation = new ArrayList<>(); 60 List<String> errorsExemptAnnotation = new ArrayList<>(); 61 62 for (int i = 0; i < classes.length; i++) { 63 String className = classes[i]; 64 Field[] fields = Class.forName(className).getDeclaredFields(); 65 for (int j = 0; j < fields.length; j++) { 66 Field field = fields[j]; 67 68 // These are some internal fields 69 if (field.isSynthetic()) continue; 70 71 boolean isAnnotated = containsAddedInAnnotation(field, addedInOrBeforeApis, 72 annotationClasses); 73 boolean shouldBeAnnotated = Modifier.isPublic(field.getModifiers()) 74 || Modifier.isProtected(field.getModifiers()); 75 76 if (!shouldBeAnnotated && isAnnotated) { 77 errorsExtraAnnotation.add(className + " FIELD: " + field.getName()); 78 } 79 80 if (shouldBeAnnotated && !isAnnotated) { 81 errorsNoAnnotation.add(className + " FIELD: " + field.getName()); 82 } 83 } 84 85 Method[] methods = Class.forName(className).getDeclaredMethods(); 86 for (int j = 0; j < methods.length; j++) { 87 Method method = methods[j]; 88 89 // These are some internal methods 90 if (method.isBridge() || method.isSynthetic()) continue; 91 92 boolean isAnnotated = containsAddedInAnnotation(method, addedInOrBeforeApis, 93 annotationClasses); 94 boolean shouldBeAnnotated = Modifier.isPublic(method.getModifiers()) 95 || Modifier.isProtected(method.getModifiers()); 96 97 if (isExempt(method)) { 98 if (isAnnotated) { 99 errorsExemptAnnotation.add(className + " METHOD: " + method.getName()); 100 } 101 continue; 102 } 103 104 if (!shouldBeAnnotated && isAnnotated) { 105 errorsExtraAnnotation.add(className + " METHOD: " + method.getName()); 106 } 107 108 if (shouldBeAnnotated && !isAnnotated) { 109 errorsNoAnnotation.add(className + " METHOD: " + method.getName()); 110 } 111 } 112 } 113 114 StringBuilder errorFlatten = new StringBuilder(); 115 116 if (!errorsExemptAnnotation.isEmpty()) { 117 errorFlatten.append( 118 "Errors:\nApiRequirements or AddedInOrBefore annotation used for overridden " 119 + "JDK methods-\n"); 120 errorFlatten.append(String.join("\n", errorsExemptAnnotation)); 121 } 122 123 if (!errorsNoAnnotation.isEmpty()) { 124 List<Class<?>> annotations = Arrays.stream(annotationClasses).toList(); 125 if (annotations.isEmpty()) { 126 errorFlatten.append("Errors:\nannotationClasses argument should not be empty\n"); 127 } else { 128 if (annotations.contains(android.car.annotation.ApiRequirements.class)) { 129 errorFlatten.append("\nErrors:\nMissing ApiRequirements annotation for-\n"); 130 } else { 131 errorFlatten.append("\nErrors:\nMissing AddedIn annotation for-\n"); 132 } 133 } 134 errorFlatten.append(String.join("\n", errorsNoAnnotation)); 135 } 136 137 if (!errorsExtraAnnotation.isEmpty()) { 138 // TODO(b/240343308): remove @AddedIn once all usages have been replaced 139 errorFlatten.append("\nErrors:\nApiRequirements annotation used for " 140 + "private or package scoped members or methods-\n"); 141 errorFlatten.append(String.join("\n", errorsExtraAnnotation)); 142 } 143 144 assertWithMessage(errorFlatten.toString()).that( 145 errorsExtraAnnotation.size() + errorsNoAnnotation.size() 146 + errorsExemptAnnotation.size()).isEqualTo(0); 147 } 148 checkForAnnotation(String[] classes, Class<?>... annotationClasses)149 public static void checkForAnnotation(String[] classes, Class<?>... annotationClasses) 150 throws Exception { 151 checkForAnnotation(classes, null, annotationClasses); 152 } 153 154 @SuppressWarnings("unchecked") containsAddedInAnnotation(Field field, HashSet<String> addedInOrBeforeApis, Class<?>... annotationClasses)155 private static boolean containsAddedInAnnotation(Field field, 156 HashSet<String> addedInOrBeforeApis, Class<?>... annotationClasses) { 157 for (int i = 0; i < annotationClasses.length; i++) { 158 if (field.getAnnotation((Class<Annotation>) annotationClasses[i]) != null) { 159 validatedAddInOrBeforeAnnotation(field, addedInOrBeforeApis); 160 return true; 161 } 162 } 163 return false; 164 } 165 166 @SuppressWarnings("unchecked") containsAddedInAnnotation(Method method, HashSet<String> addedInOrBeforeApis, Class<?>... annotationClasses)167 private static boolean containsAddedInAnnotation(Method method, 168 HashSet<String> addedInOrBeforeApis, Class<?>... annotationClasses) { 169 for (int i = 0; i < annotationClasses.length; i++) { 170 if (method.getAnnotation((Class<Annotation>) annotationClasses[i]) != null) { 171 validatedAddInOrBeforeAnnotation(method, addedInOrBeforeApis); 172 return true; 173 } 174 } 175 return false; 176 } 177 validatedAddInOrBeforeAnnotation(Field field, HashSet<String> addedInOrBeforeApis)178 private static void validatedAddInOrBeforeAnnotation(Field field, 179 HashSet<String> addedInOrBeforeApis) { 180 AddedInOrBefore annotation = field.getAnnotation(AddedInOrBefore.class); 181 String fullFieldName = 182 (field.getDeclaringClass().getName() + "." + field.getName()).replace('$', '.'); 183 if (annotation != null) { 184 assertWithMessage( 185 "%s, field: %s should not use AddedInOrBefore annotation. The annotation was " 186 + "reserved only for APIs added in or before majorVersion:33, " 187 + "minorVersion:0", 188 field.getDeclaringClass(), field.getName()) 189 .that(annotation.majorVersion()).isEqualTo(33); 190 assertWithMessage( 191 "%s, field: %s should not use AddedInOrBefore annotation. The annotation was " 192 + "reserved only for APIs added in or before majorVersion:33, " 193 + "minorVersion:0", 194 field.getDeclaringClass(), field.getName()) 195 .that(annotation.minorVersion()).isEqualTo(0); 196 if (addedInOrBeforeApis != null) { 197 assertWithMessage( 198 "%s, field: %s was newly added and should not use the AddedInOrBefore " 199 + "annotation.", 200 field.getDeclaringClass(), field.getName()) 201 .that(addedInOrBeforeApis.contains(fullFieldName)).isTrue(); 202 } 203 } 204 } 205 validatedAddInOrBeforeAnnotation(Method method, HashSet<String> addedInOrBeforeApis)206 private static void validatedAddInOrBeforeAnnotation(Method method, 207 HashSet<String> addedInOrBeforeApis) { 208 AddedInOrBefore annotation = method.getAnnotation(AddedInOrBefore.class); 209 String fullMethodName = 210 (method.getDeclaringClass().getName() + "." + method.getName()).replace('$', '.'); 211 if (annotation != null) { 212 assertWithMessage( 213 "%s, method: %s should not use AddedInOrBefore annotation. The annotation was " 214 + "reserved only for APIs added in or before majorVersion:33, " 215 + "minorVersion:0", 216 method.getDeclaringClass(), method.getName()) 217 .that(annotation.majorVersion()).isEqualTo(33); 218 assertWithMessage( 219 "%s, method: %s should not use AddedInOrBefore annotation. The annotation was " 220 + "reserved only for APIs added in or before majorVersion:33, " 221 + "minorVersion:0", 222 method.getDeclaringClass(), method.getName()) 223 .that(annotation.minorVersion()).isEqualTo(0); 224 if (addedInOrBeforeApis != null) { 225 assertWithMessage( 226 "%s, method: %s was newly added and should not use the AddedInOrBefore " 227 + "annotation.", 228 method.getDeclaringClass(), method.getName()) 229 .that(addedInOrBeforeApis.contains(fullMethodName)).isTrue(); 230 } 231 } 232 } 233 234 // Overridden JDK methods do not need @ApiRequirements annotations. Since @Override 235 // annotations are discarded by the compiler, one needs to manually add any classes where 236 // methods are overridden and @ApiRequirements are not needed. 237 // Currently, overridden methods from java.lang.Object should be skipped. isExempt(Method method)238 private static boolean isExempt(Method method) { 239 String methodSignature = 240 method.getReturnType().toString() + method.getName() + Arrays.toString( 241 method.getParameterTypes()); 242 return sJavaLangObjectNames.contains(methodSignature); 243 } 244 } 245