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