1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /*
4  *******************************************************************************
5  * Copyright (C) 2014, International Business Machines Corporation and         *
6  * others. All Rights Reserved.                                                *
7  *******************************************************************************
8  */
9 package com.ibm.icu.dev.tool.docs;
10 
11 import java.io.File;
12 import java.io.PrintWriter;
13 import java.lang.reflect.Constructor;
14 import java.lang.reflect.Field;
15 import java.lang.reflect.GenericArrayType;
16 import java.lang.reflect.Method;
17 import java.lang.reflect.Modifier;
18 import java.lang.reflect.ParameterizedType;
19 import java.lang.reflect.Type;
20 import java.lang.reflect.TypeVariable;
21 import java.lang.reflect.WildcardType;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.Set;
27 import java.util.TreeMap;
28 
29 public class DeprecatedAPIChecker {
30 
main(String[] args)31     public static void main(String[] args) {
32         if (args.length != 1) {
33             System.err.println("Illegal command argument. Specify the API signature file path.");
34         }
35         // Load the ICU4J API signature file
36         Set<APIInfo> apiInfoSet = APIData.read(new File(args[0]), true).getAPIInfoSet();
37 
38         DeprecatedAPIChecker checker = new DeprecatedAPIChecker(apiInfoSet, new PrintWriter(System.err, true));
39         checker.checkDeprecated();
40         System.exit(checker.errCount);
41     }
42 
43     private int errCount = 0;
44     private Set<APIInfo> apiInfoSet;
45     private PrintWriter pw;
46 
DeprecatedAPIChecker(Set<APIInfo> apiInfoSet, PrintWriter pw)47     public DeprecatedAPIChecker(Set<APIInfo> apiInfoSet, PrintWriter pw) {
48         this.apiInfoSet = apiInfoSet;
49         this.pw = pw;
50     }
51 
errorCount()52     public int errorCount() {
53         return errCount;
54     }
55 
checkDeprecated()56     public void checkDeprecated() {
57         // Gather API class/enum names and its names that can be
58         // used for Class.forName()
59         Map<String, String> apiClassNameMap = new TreeMap<String, String>();
60         for (APIInfo api : apiInfoSet) {
61             if (!api.isPublic() && !api.isProtected()) {
62                 continue;
63             }
64             if (!api.isClass() && !api.isEnum()) {
65                 continue;
66             }
67             String packageName = api.getPackageName();
68             String className = api.getName();
69 
70             // Replacing separator for nested class/enum (replacing '.' with
71             // '$'), so we can use the name for Class.forName(String)
72             String classNamePath = className.contains(".") ? className.replace('.', '$') : className;
73 
74             apiClassNameMap.put(packageName + "." + classNamePath, packageName + "." + className);
75         }
76 
77         // Walk through API classes using reflection
78         for (Entry<String, String> classEntry : apiClassNameMap.entrySet()) {
79             String classNamePath = classEntry.getKey();
80             try {
81                 Class<?> cls = Class.forName(classNamePath);
82                 if (cls.isEnum()) {
83                     checkEnum(cls, apiClassNameMap);
84                 } else {
85                     checkClass(cls, apiClassNameMap);
86                 }
87             } catch (ClassNotFoundException e) {
88                 pw.println("## Error ## Class " + classNamePath + " is not found.");
89                 errCount++;
90             }
91         }
92     }
93 
checkClass(Class<?> cls, Map<String, String> clsNameMap)94     private void checkClass(Class<?> cls, Map<String, String> clsNameMap) {
95         assert !cls.isEnum();
96 
97         String clsPath = cls.getName();
98         String clsName = clsNameMap.get(clsPath);
99         APIInfo api = null;
100 
101         if (clsName != null) {
102             api = findClassInfo(apiInfoSet, clsName);
103         }
104         if (api == null) {
105             pw.println("## Error ## Class " + clsName + " is not found in the API signature data.");
106             errCount++;
107         }
108 
109         // check class
110         compareDeprecated(isAPIDeprecated(api), cls.isAnnotationPresent(Deprecated.class), clsName, null, "Class");
111 
112         // check fields
113         for (Field f : cls.getDeclaredFields()) {
114             if (!isPublicOrProtected(f.getModifiers())) {
115                 continue;
116             }
117 
118             String fName = f.getName();
119             api = findFieldInfo(apiInfoSet, clsName, fName);
120             if (api == null) {
121                 pw.println("## Error ## Field " + clsName + "." + fName + " is not found in the API signature data.");
122                 errCount++;
123                 continue;
124             }
125 
126             compareDeprecated(isAPIDeprecated(api), f.isAnnotationPresent(Deprecated.class), clsName, fName, "Field");
127         }
128 
129         // check constructors
130         for (Constructor<?> ctor : cls.getDeclaredConstructors()) {
131             if (!isPublicOrProtected(ctor.getModifiers())) {
132                 continue;
133             }
134 
135             List<String> paramNames = getParamNames(ctor);
136             api = findConstructorInfo(apiInfoSet, clsName, paramNames);
137 
138             if (api == null) {
139                 pw.println("## Error ## Constructor " + clsName + formatParams(paramNames)
140                         + " is not found in the API signature data.");
141                 errCount++;
142                 continue;
143             }
144 
145             compareDeprecated(isAPIDeprecated(api), ctor.isAnnotationPresent(Deprecated.class), clsName,
146                     api.getClassName() + formatParams(paramNames), "Constructor");
147         }
148 
149         // check methods
150         for (Method mtd : cls.getDeclaredMethods()) {
151             // Note: We exclude synthetic method.
152             if (!isPublicOrProtected(mtd.getModifiers()) || mtd.isSynthetic()) {
153                 continue;
154             }
155 
156             String mtdName = mtd.getName();
157             List<String> paramNames = getParamNames(mtd);
158             api = findMethodInfo(apiInfoSet, clsName, mtdName, paramNames);
159 
160             if (api == null) {
161                 pw.println("## Error ## Method " + clsName + "#" + mtdName + formatParams(paramNames)
162                         + " is not found in the API signature data.");
163                 errCount++;
164                 continue;
165             }
166 
167             compareDeprecated(isAPIDeprecated(api), mtd.isAnnotationPresent(Deprecated.class), clsName, mtdName
168                     + formatParams(paramNames), "Method");
169 
170         }
171     }
172 
checkEnum(Class<?> cls, Map<String, String> clsNameMap)173     private void checkEnum(Class<?> cls, Map<String, String> clsNameMap) {
174         assert cls.isEnum();
175 
176         String enumPath = cls.getName();
177         String enumName = clsNameMap.get(enumPath);
178         APIInfo api = null;
179 
180         if (enumName != null) {
181             api = findEnumInfo(apiInfoSet, enumName);
182         }
183         if (api == null) {
184             pw.println("## Error ## Enum " + enumName + " is not found in the API signature data.");
185             errCount++;
186         }
187 
188         // check enum
189         compareDeprecated(isAPIDeprecated(api), cls.isAnnotationPresent(Deprecated.class), enumName, null, "Enum");
190 
191         // check enum constants
192         for (Field ec : cls.getDeclaredFields()) {
193             if (!ec.isEnumConstant()) {
194                 continue;
195             }
196             String ecName = ec.getName();
197             api = findEnumConstantInfo(apiInfoSet, enumName, ecName);
198             if (api == null) {
199                 pw.println("## Error ## Enum constant " + enumName + "." + ecName
200                         + " is not found in the API signature data.");
201                 errCount++;
202                 continue;
203             }
204 
205             compareDeprecated(isAPIDeprecated(api), ec.isAnnotationPresent(Deprecated.class), enumName, ecName,
206                     "Enum Constant");
207         }
208 
209         // check methods
210         for (Method mtd : cls.getDeclaredMethods()) {
211             // Note: We exclude built-in methods in a Java Enum instance
212             if (!isPublicOrProtected(mtd.getModifiers()) || isBuiltinEnumMethod(mtd)) {
213                 continue;
214             }
215 
216             String mtdName = mtd.getName();
217             List<String> paramNames = getParamNames(mtd);
218             api = findMethodInfo(apiInfoSet, enumName, mtdName, paramNames);
219 
220             if (api == null) {
221                 pw.println("## Error ## Method " + enumName + "#" + mtdName + formatParams(paramNames)
222                         + " is not found in the API signature data.");
223                 errCount++;
224                 continue;
225             }
226 
227             compareDeprecated(isAPIDeprecated(api), mtd.isAnnotationPresent(Deprecated.class), enumName, mtdName
228                     + formatParams(paramNames), "Method");
229 
230         }
231     }
232 
compareDeprecated(boolean depTag, boolean depAnt, String cls, String name, String type)233     private void compareDeprecated(boolean depTag, boolean depAnt, String cls, String name, String type) {
234         if (depTag != depAnt) {
235             String apiName = cls;
236             if (name != null) {
237                 apiName += "." + name;
238             }
239             if (depTag) {
240                 pw.println("No @Deprecated annotation: [" + type + "] " + apiName);
241             } else {
242                 pw.println("No @deprecated JavaDoc tag: [" + type + "] " + apiName);
243             }
244             errCount++;
245         }
246     }
247 
isPublicOrProtected(int modifier)248     private static boolean isPublicOrProtected(int modifier) {
249         return ((modifier & Modifier.PUBLIC) != 0) || ((modifier & Modifier.PROTECTED) != 0);
250     }
251 
252     // Check if a method is automatically generated for a each Enum
isBuiltinEnumMethod(Method mtd)253     private static boolean isBuiltinEnumMethod(Method mtd) {
254         // Just check method name for now
255         String name = mtd.getName();
256         return name.equals("values") || name.equals("valueOf");
257     }
258 
isAPIDeprecated(APIInfo api)259     private static boolean isAPIDeprecated(APIInfo api) {
260         return api.isDeprecated() || api.isInternal() || api.isObsolete();
261     }
262 
findClassInfo(Set<APIInfo> apis, String cls)263     private static APIInfo findClassInfo(Set<APIInfo> apis, String cls) {
264         for (APIInfo api : apis) {
265             String clsName = api.getPackageName() + "." + api.getName();
266             if (api.isClass() && clsName.equals(cls)) {
267                 return api;
268             }
269         }
270         return null;
271     }
272 
findFieldInfo(Set<APIInfo> apis, String cls, String field)273     private static APIInfo findFieldInfo(Set<APIInfo> apis, String cls, String field) {
274         for (APIInfo api : apis) {
275             String clsName = api.getPackageName() + "." + api.getClassName();
276             if (api.isField() && clsName.equals(cls) && api.getName().equals(field)) {
277                 return api;
278             }
279         }
280         return null;
281     }
282 
findConstructorInfo(Set<APIInfo> apis, String cls, List<String> params)283     private static APIInfo findConstructorInfo(Set<APIInfo> apis, String cls, List<String> params) {
284         for (APIInfo api : apis) {
285             String clsName = api.getPackageName() + "." + api.getClassName();
286             if (api.isConstructor() && clsName.equals(cls)) {
287                 // check params
288                 List<String> paramsFromApi = getParamNames(api);
289                 if (paramsFromApi.size() == params.size()) {
290                     boolean match = true;
291                     for (int i = 0; i < params.size(); i++) {
292                         if (!params.get(i).equals(paramsFromApi.get(i))) {
293                             match = false;
294                             break;
295                         }
296                     }
297                     if (match) {
298                         return api;
299                     }
300                 }
301             }
302         }
303         return null;
304     }
305 
findMethodInfo(Set<APIInfo> apis, String cls, String method, List<String> params)306     private static APIInfo findMethodInfo(Set<APIInfo> apis, String cls, String method, List<String> params) {
307         for (APIInfo api : apis) {
308             String clsName = api.getPackageName() + "." + api.getClassName();
309             if (api.isMethod() && clsName.equals(cls) && api.getName().equals(method)) {
310                 // check params
311                 List<String> paramsFromApi = getParamNames(api);
312                 if (paramsFromApi.size() == params.size()) {
313                     boolean match = true;
314                     for (int i = 0; i < params.size(); i++) {
315                         if (!params.get(i).equals(paramsFromApi.get(i))) {
316                             match = false;
317                             break;
318                         }
319                     }
320                     if (match) {
321                         return api;
322                     }
323                 }
324             }
325         }
326         return null;
327     }
328 
findEnumInfo(Set<APIInfo> apis, String ecls)329     private static APIInfo findEnumInfo(Set<APIInfo> apis, String ecls) {
330         for (APIInfo api : apis) {
331             String clsName = api.getPackageName() + "." + api.getName();
332             if (api.isEnum() && clsName.equals(ecls)) {
333                 return api;
334             }
335         }
336         return null;
337     }
338 
findEnumConstantInfo(Set<APIInfo> apis, String ecls, String econst)339     private static APIInfo findEnumConstantInfo(Set<APIInfo> apis, String ecls, String econst) {
340         for (APIInfo api : apis) {
341             String clsName = api.getPackageName() + "." + api.getClassName();
342             if (api.isEnumConstant() && clsName.equals(ecls) && api.getName().equals(econst)) {
343                 return api;
344             }
345         }
346         return null;
347     }
348 
getParamNames(APIInfo api)349     private static List<String> getParamNames(APIInfo api) {
350         if (!api.isMethod() && !api.isConstructor()) {
351             throw new IllegalArgumentException(api.toString() + " is not a constructor or a method.");
352         }
353 
354         List<String> nameList = new ArrayList<String>();
355         String signature = api.getSignature();
356         int start = signature.indexOf('(');
357         int end = signature.indexOf(')');
358 
359         if (start < 0 || end < 0 || start > end) {
360             throw new RuntimeException(api.toString() + " has bad API signature: " + signature);
361         }
362 
363         String paramsSegment = signature.substring(start + 1, end);
364         // erase generic args
365         if (paramsSegment.indexOf('<') >= 0) {
366             StringBuilder buf = new StringBuilder();
367             boolean inGenericsParams = false;
368             for (int i = 0; i < paramsSegment.length(); i++) {
369                 char c = paramsSegment.charAt(i);
370                 if (inGenericsParams) {
371                     if (c == '>') {
372                         inGenericsParams = false;
373                     }
374                 } else {
375                     if (c == '<') {
376                         inGenericsParams = true;
377                     } else {
378                         buf.append(c);
379                     }
380                 }
381             }
382             paramsSegment = buf.toString();
383         }
384 
385         if (paramsSegment.length() > 0) {
386             String[] params = paramsSegment.split("\\s*,\\s*");
387             for (String p : params) {
388                 if (p.endsWith("...")) {
389                     // varargs to array
390                     p = p.substring(0, p.length() - 3) + "[]";
391                 }
392                 nameList.add(p);
393             }
394         }
395 
396         return nameList;
397     }
398 
getParamNames(Constructor<?> ctor)399     private static List<String> getParamNames(Constructor<?> ctor) {
400         return toTypeNameList(ctor.getGenericParameterTypes());
401     }
402 
getParamNames(Method method)403     private static List<String> getParamNames(Method method) {
404         return toTypeNameList(method.getGenericParameterTypes());
405     }
406 
407     private static final String[] PRIMITIVES = { "byte", "short", "int", "long", "float", "double", "boolean", "char" };
408     private static char[] PRIMITIVE_SIGNATURES = { 'B', 'S', 'I', 'J', 'F', 'D', 'Z', 'C' };
409 
toTypeNameList(Type[] types)410     private static List<String> toTypeNameList(Type[] types) {
411         List<String> nameList = new ArrayList<String>();
412 
413         for (Type t : types) {
414             StringBuilder s = new StringBuilder();
415             if (t instanceof ParameterizedType) {
416                 // throw away generics parameters
417                 ParameterizedType prdType = (ParameterizedType) t;
418                 Class<?> rawType = (Class<?>) prdType.getRawType();
419                 s.append(rawType.getCanonicalName());
420             } else if (t instanceof WildcardType) {
421                 // we don't need to worry about WildcardType,
422                 // because this tool erases generics parameters
423                 // for comparing method/constructor parameters
424                 throw new RuntimeException("WildcardType not supported by this tool");
425             } else if (t instanceof TypeVariable) {
426                 // this tool does not try to resolve actual parameter
427                 // type - for example, "<T extends Object> void foo(T in)"
428                 // this tool just use the type variable "T" for API signature
429                 // comparison. This is actually not perfect, but should be
430                 // sufficient for our purpose.
431                 TypeVariable<?> tVar = (TypeVariable<?>) t;
432                 s.append(tVar.getName());
433             } else if (t instanceof GenericArrayType) {
434                 // same as TypeVariable. "T[]" is sufficient enough.
435                 GenericArrayType tGenArray = (GenericArrayType) t;
436                 s.append(tGenArray.toString());
437             } else if (t instanceof Class) {
438                 Class<?> tClass = (Class<?>) t;
439                 String tName = tClass.getCanonicalName();
440 
441                 if (tName.charAt(0) == '[') {
442                     // Array type
443                     int idx = 0;
444                     for (; idx < tName.length(); idx++) {
445                         if (tName.charAt(idx) != '[') {
446                             break;
447                         }
448                     }
449                     int dimension = idx;
450                     char sigChar = tName.charAt(dimension);
451 
452                     String elemType = null;
453                     if (sigChar == 'L') {
454                         // class
455                         elemType = tName.substring(dimension + 1, tName.length() - 1);
456                     } else {
457                         // primitive
458                         for (int i = 0; i < PRIMITIVE_SIGNATURES.length; i++) {
459                             if (sigChar == PRIMITIVE_SIGNATURES[i]) {
460                                 elemType = PRIMITIVES[i];
461                                 break;
462                             }
463                         }
464                     }
465 
466                     if (elemType == null) {
467                         throw new RuntimeException("Unexpected array type: " + tName);
468                     }
469 
470                     s.append(elemType);
471                     for (int i = 0; i < dimension; i++) {
472                         s.append("[]");
473                     }
474                 } else {
475                     s.append(tName);
476                 }
477             } else {
478                 throw new IllegalArgumentException("Unknown type: " + t);
479             }
480 
481             nameList.add(s.toString());
482         }
483 
484         return nameList;
485     }
486 
formatParams(List<String> paramNames)487     private static String formatParams(List<String> paramNames) {
488         StringBuilder buf = new StringBuilder("(");
489         boolean isFirst = true;
490         for (String p : paramNames) {
491             if (isFirst) {
492                 isFirst = false;
493             } else {
494                 buf.append(", ");
495             }
496             buf.append(p);
497         }
498         buf.append(")");
499 
500         return buf.toString();
501     }
502 }
503