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