1 /*
2  * Copyright (C) 2023 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 com.android.car.tool.apibuilder;
18 
19 import com.android.car.tool.data.ClassData;
20 import com.android.car.tool.data.ConstructorData;
21 import com.android.car.tool.data.FieldData;
22 import com.android.car.tool.data.MethodData;
23 import com.android.car.tool.data.PackageData;
24 import com.android.car.tool.data.ParsedData;
25 
26 import com.github.javaparser.ast.expr.MethodCallExpr;
27 
28 import java.util.ArrayList;
29 import java.util.List;
30 
31 public final class ParsedDataHelper {
32 
33     private static final ArrayList<String> EXEMPT_METHODS = new ArrayList<>(
34             List.of("toString", "equals", "hashCode", "finalize", "writeToParcel",
35                     "describeContents"));
36 
getClassNamesOnly(ParsedData parsedData)37     public static List<String> getClassNamesOnly(ParsedData parsedData) {
38         List<String> classes = new ArrayList<>();
39         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
40                 .forEach((classData) -> classes.add(classData.useableClassName)));
41         return classes;
42     }
43 
44     /**
45      * Returns all the non-hidden classes
46      */
getNonHiddenClassNamesOnly(ParsedData parsedData)47     public static List<String> getNonHiddenClassNamesOnly(ParsedData parsedData) {
48         List<String> classes = new ArrayList<>();
49         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
50                 .forEach((classData) -> {
51                     if (!classData.annotationData.isSystemApi && classData.isClassHidden) {
52                         return;
53                     }
54                     classes.add(classData.useableClassName);
55                 }));
56         return classes;
57     }
58 
getAddedInOrBeforeApisOnly(ParsedData parsedData)59     public static List<String> getAddedInOrBeforeApisOnly(ParsedData parsedData) {
60         List<String> fieldsAndMethods = new ArrayList<>();
61         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
62                 .forEach((classData) -> classData.fields.values().forEach(
63                         (field) -> {
64                             if (field.annotationData.hasAddedInOrBefore) {
65                                 fieldsAndMethods.add(packageData.packageName + "."
66                                         + classData.onlyClassName + "." + field.fieldName);
67                             }
68                         })));
69         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
70                 .forEach((classData) -> classData.methods.values().forEach(
71                         (method) -> {
72                             if (method.annotationData.hasAddedInOrBefore) {
73                                 fieldsAndMethods.add(packageData.packageName + "."
74                                         + classData.onlyClassName + "." + method.methodName);
75                             }
76                         })));
77         return fieldsAndMethods;
78     }
79 
getHiddenApisOnly(ParsedData parsedData)80     public static List<String> getHiddenApisOnly(ParsedData parsedData) {
81         List<String> fieldsAndMethods = new ArrayList<>();
82         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
83                 .forEach((classData) -> classData.fields.values().forEach(
84                         (field) -> {
85                             if ((!field.annotationData.isSystemApi && field.isHidden)
86                                     || (classData.isClassHidden
87                                             && !classData.annotationData.isSystemApi)) {
88                                 fieldsAndMethods
89                                         .add(formatFieldString(packageData, classData, field));
90                             }
91                         })));
92         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
93                 .forEach((classData) -> classData.methods.values().forEach(
94                         (method) -> {
95                             if ((!method.annotationData.isSystemApi && method.isHidden)
96                                     || (classData.isClassHidden
97                                             && !classData.annotationData.isSystemApi)) {
98                                 fieldsAndMethods
99                                         .add(formatMethodString(packageData, classData, method));
100                             }
101                         })));
102         return fieldsAndMethods;
103     }
104 
getHiddenApisWithHiddenConstructor(ParsedData parsedData)105     public static List<String> getHiddenApisWithHiddenConstructor(ParsedData parsedData) {
106         List<String> allHiddenApis = getHiddenApisOnly(parsedData);
107 
108         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
109                 .forEach((classData) -> classData.constructors.values()
110                         .forEach((constructorData) -> {
111                             if ((constructorData.isHidden
112                                     && !constructorData.annotationData.isSystemApi)
113                                     || (classData.isClassHidden
114                                             && !classData.annotationData.isSystemApi)) {
115                                 allHiddenApis.add(formatConstructorString(packageData, classData,
116                                         constructorData));
117                             }
118                         })));
119 
120         return allHiddenApis;
121     }
122 
getAllApis(ParsedData parsedData)123     public static List<String> getAllApis(ParsedData parsedData) {
124         List<String> fieldsAndMethods = new ArrayList<>();
125         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
126                 .forEach((classData) -> classData.fields.values().forEach(
127                         (field) -> {
128                             fieldsAndMethods.add(formatFieldString(packageData, classData, field));
129                         })));
130         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
131                 .forEach((classData) -> classData.methods.values().forEach(
132                         (method) -> {
133                             fieldsAndMethods
134                                     .add(formatMethodString(packageData, classData, method));
135                         })));
136         return fieldsAndMethods;
137     }
138 
getAllApisWithConstructor(ParsedData parsedData)139     public static List<String> getAllApisWithConstructor(ParsedData parsedData) {
140         List<String> allApis = getAllApis(parsedData);
141 
142         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
143                 .forEach((classData) -> classData.constructors.values()
144                         .forEach((constructorData) -> {
145                             allApis.add(formatConstructorString(packageData, classData,
146                                     constructorData));
147                         })));
148 
149         return allApis;
150     }
151 
checkAssertPlatformVersionAtLeast( ParsedData parsedData)152     public static List<String> checkAssertPlatformVersionAtLeast(
153             ParsedData parsedData) {
154         List<String> apis = new ArrayList<>();
155         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
156                 .forEach((classData) -> classData.methods.values().forEach(
157                         (method) -> {
158                             // Only check that assertPlatformVersionAtLeast is present for APIs
159                             // added after TIRAMISU_x.
160                             if (!method.annotationData.hasApiRequirementAnnotation
161                                     || method.annotationData.minPlatformVersion.contains(
162                                     "TIRAMISU") || method.firstBodyStatement == null) {
163                                 return;
164                             }
165 
166                             for (String exempt : EXEMPT_METHODS) {
167                                 if (method.methodName.contains(exempt)) {
168                                     return;
169                                 }
170                             }
171 
172                             int line = 0;
173                             if (method.firstBodyStatement.getBegin().isPresent()) {
174                                 line = method.firstBodyStatement.getBegin().get().line;
175                             }
176 
177                             // Case where the first body statement is not a method call expression.
178                             if (!method.firstBodyStatement.isExpressionStmt()
179                                     || !method.firstBodyStatement.asExpressionStmt()
180                                             .getExpression().isMethodCallExpr()) {
181                                 apis.add(formatMethodString(packageData, classData, method) + " | "
182                                         + line + " | " + method.fileName);
183                                 return;
184                             }
185 
186                             MethodCallExpr methodCallExpr = (MethodCallExpr)
187                                     method.firstBodyStatement.asExpressionStmt().getExpression();
188 
189                             // Case where no `assertPlatformVersionAtLeastU` method call exists in
190                             // the first line of the method body.
191                             // TODO(b/280357275): Add the version (ex. U, V, ...) as an argument
192                             if (!methodCallExpr.getName().asString().contains(
193                                     "assertPlatformVersionAtLeastU")) {
194                                 apis.add(formatMethodString(packageData, classData, method) + " | "
195                                         + line + " | " + method.fileName);
196                             }
197                         })));
198         return apis;
199     }
200 
getApisWithVersion(ParsedData parsedData)201     public static List<String> getApisWithVersion(ParsedData parsedData) {
202         List<String> apisWithVersion = new ArrayList<>();
203 
204         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
205                 .forEach((classData) -> classData.fields.values().forEach(
206                         (field) -> {
207                             String minCarVersion = "";
208                             if (field.annotationData.hasAddedInAnnotation) {
209                                 minCarVersion = field.annotationData.addedInPlatformVersion;
210                             } else if (field.annotationData.hasAddedInOrBefore) {
211                                 // The only car version for @AddedInOrBefore is TIRAMISU_0.
212                                 minCarVersion = "TIRAMISU_0";
213                             } else {
214                                 minCarVersion = field.annotationData.minCarVersion;
215                             }
216                             apisWithVersion.add(
217                                     formatFieldString(packageData, classData, field) + " | "
218                                             + minCarVersion);
219                         })));
220         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
221                 .forEach((classData) -> classData.methods.values().forEach(
222                         (method) -> {
223                             String minCarVersion = "";
224                             if (method.annotationData.hasAddedInAnnotation) {
225                                 minCarVersion = method.annotationData.addedInPlatformVersion;
226                             } else if (method.annotationData.hasAddedInOrBefore) {
227                                 // The only car version for @AddedInOrBefore is TIRAMISU_0.
228                                 minCarVersion = "TIRAMISU_0";
229                             } else {
230                                 minCarVersion = method.annotationData.minCarVersion;
231                             }
232                             apisWithVersion.add(
233                                     formatMethodString(packageData, classData, method) + " | "
234                                             + minCarVersion);
235                         })));
236 
237         return apisWithVersion;
238     }
239 
240     /**
241      * Gives incorrect usage of requiresApi annotation in Car Service.
242      */
243     // TODO(b/277617236): add tests for this
getIncorrectRequiresApiUsage(ParsedData parsedData)244     public static List<String> getIncorrectRequiresApiUsage(ParsedData parsedData) {
245         List<String> incorrectRequiresApiUsage = new ArrayList<>();
246         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
247                 .forEach((classData) -> {
248                     if (classData.annotationData.hasRequiresApiAnnotation) {
249                         incorrectRequiresApiUsage.add(classData.useableClassName + " "
250                                 + classData.annotationData.requiresApiVersion);
251                     }
252                 }));
253         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
254                 .forEach((classData) -> classData.methods.values().forEach(
255                         (method) -> {
256                             if (method.annotationData.hasRequiresApiAnnotation) {
257                                 incorrectRequiresApiUsage
258                                         .add(formatMethodString(packageData, classData, method)
259                                                 + " " + method.annotationData.requiresApiVersion);
260                             }
261                         })));
262         return incorrectRequiresApiUsage;
263     }
264 
265     /**
266      * Gives incorrect usage of AddedIn annotation in Car built-in library.
267      */
268     // TODO(b/277617236): add tests for this
getIncorrectRequiresApi(ParsedData parsedData)269     public static List<String> getIncorrectRequiresApi(ParsedData parsedData) {
270         List<String> incorrectRequiresApi = new ArrayList<>();
271         parsedData.packages.values().forEach((packageData) -> packageData.classes.values()
272                 .forEach((classData) -> classData.methods.values().forEach(
273                         (method) -> {
274                             if (method.annotationData.hasAddedInAnnotation
275                                     && !method.annotationData.addedInPlatformVersion
276                                             .contains("TIRAMISU")) {
277                                 if (!method.annotationData.hasRequiresApiAnnotation) {
278                                     // Require API annotation is missing.
279                                     incorrectRequiresApi.add(
280                                             formatMethodString(packageData, classData, method));
281                                 }
282                                 String platformVersion =
283                                         method.annotationData.addedInPlatformVersion;
284                                 String platformVersionWithoutMinorVersion = platformVersion
285                                         .substring(0, platformVersion.length() - 2);
286                                 if (method.annotationData.hasRequiresApiAnnotation
287                                         && !method.annotationData.requiresApiVersion
288                                                 .equals(platformVersionWithoutMinorVersion)) {
289                                     // requires Api annotation is wrong
290                                     incorrectRequiresApi.add(
291                                             formatMethodString(packageData, classData, method));
292                                 }
293                             }
294                         })));
295         return incorrectRequiresApi;
296     }
297 
formatMethodString(PackageData packageData, ClassData classData, MethodData method)298     private static String formatMethodString(PackageData packageData, ClassData classData,
299             MethodData method) {
300         return packageData.packageName + " "
301                 + classData.onlyClassName + " " + method.returnType + " "
302                 + method.fullMethodname;
303     }
304 
formatFieldString(PackageData packageData, ClassData classData, FieldData field)305     private static String formatFieldString(PackageData packageData, ClassData classData,
306             FieldData field) {
307         return packageData.packageName + " "
308                 + classData.onlyClassName + " " + field.fieldType + " "
309                 + field.fieldName;
310     }
311 
formatConstructorString(PackageData packageData, ClassData classData, ConstructorData constructorData)312     private static String formatConstructorString(PackageData packageData, ClassData classData,
313             ConstructorData constructorData) {
314         return packageData.packageName + " "
315                 + classData.onlyClassName + " "
316                 + constructorData.constructorName + " "
317                 + constructorData.fullConstructorName;
318     }
319 }
320