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<>(); 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 137 Class<?> declClass = cls.getDeclaringClass(); 138 if (declClass != null && !Modifier.isStatic(cls.getModifiers())) { 139 // This is non-static inner class's constructor. 140 // javac automatically injects instance of declaring class 141 // as the first param of the constructor, but ICU's API 142 // signature is based on javadoc and it generates signature 143 // without the implicit parameter. 144 assert paramNames.get(0).equals(declClass.getName()); 145 paramNames.remove(0); 146 } 147 148 api = findConstructorInfo(apiInfoSet, clsName, paramNames); 149 150 if (api == null) { 151 pw.println("## Error ## Constructor " + clsName + formatParams(paramNames) 152 + " is not found in the API signature data."); 153 errCount++; 154 continue; 155 } 156 157 compareDeprecated(isAPIDeprecated(api), ctor.isAnnotationPresent(Deprecated.class), clsName, 158 api.getClassName() + formatParams(paramNames), "Constructor"); 159 } 160 161 // check methods 162 for (Method mtd : cls.getDeclaredMethods()) { 163 // Note: We exclude synthetic method. 164 if (!isPublicOrProtected(mtd.getModifiers()) || mtd.isSynthetic()) { 165 continue; 166 } 167 168 String mtdName = mtd.getName(); 169 List<String> paramNames = getParamNames(mtd); 170 api = findMethodInfo(apiInfoSet, clsName, mtdName, paramNames); 171 172 if (api == null) { 173 pw.println("## Error ## Method " + clsName + "#" + mtdName + formatParams(paramNames) 174 + " is not found in the API signature data."); 175 errCount++; 176 continue; 177 } 178 179 compareDeprecated(isAPIDeprecated(api), mtd.isAnnotationPresent(Deprecated.class), clsName, mtdName 180 + formatParams(paramNames), "Method"); 181 182 } 183 } 184 checkEnum(Class<?> cls, Map<String, String> clsNameMap)185 private void checkEnum(Class<?> cls, Map<String, String> clsNameMap) { 186 assert cls.isEnum(); 187 188 String enumPath = cls.getName(); 189 String enumName = clsNameMap.get(enumPath); 190 APIInfo api = null; 191 192 if (enumName != null) { 193 api = findEnumInfo(apiInfoSet, enumName); 194 } 195 if (api == null) { 196 pw.println("## Error ## Enum " + enumName + " is not found in the API signature data."); 197 errCount++; 198 } 199 200 // check enum 201 compareDeprecated(isAPIDeprecated(api), cls.isAnnotationPresent(Deprecated.class), enumName, null, "Enum"); 202 203 // check enum constants 204 for (Field ec : cls.getDeclaredFields()) { 205 if (!ec.isEnumConstant()) { 206 continue; 207 } 208 String ecName = ec.getName(); 209 api = findEnumConstantInfo(apiInfoSet, enumName, ecName); 210 if (api == null) { 211 pw.println("## Error ## Enum constant " + enumName + "." + ecName 212 + " is not found in the API signature data."); 213 errCount++; 214 continue; 215 } 216 217 compareDeprecated(isAPIDeprecated(api), ec.isAnnotationPresent(Deprecated.class), enumName, ecName, 218 "Enum Constant"); 219 } 220 221 // check methods 222 for (Method mtd : cls.getDeclaredMethods()) { 223 // Note: We exclude built-in methods in a Java Enum instance 224 if (!isPublicOrProtected(mtd.getModifiers()) || isBuiltinEnumMethod(mtd)) { 225 continue; 226 } 227 228 String mtdName = mtd.getName(); 229 List<String> paramNames = getParamNames(mtd); 230 api = findMethodInfo(apiInfoSet, enumName, mtdName, paramNames); 231 232 if (api == null) { 233 pw.println("## Error ## Method " + enumName + "#" + mtdName + formatParams(paramNames) 234 + " is not found in the API signature data."); 235 errCount++; 236 continue; 237 } 238 239 compareDeprecated(isAPIDeprecated(api), mtd.isAnnotationPresent(Deprecated.class), enumName, mtdName 240 + formatParams(paramNames), "Method"); 241 242 } 243 } 244 compareDeprecated(boolean depTag, boolean depAnt, String cls, String name, String type)245 private void compareDeprecated(boolean depTag, boolean depAnt, String cls, String name, String type) { 246 if (depTag != depAnt) { 247 String apiName = cls; 248 if (name != null) { 249 apiName += "." + name; 250 } 251 if (depTag) { 252 pw.println("No @Deprecated annotation: [" + type + "] " + apiName); 253 } else { 254 pw.println("No @deprecated JavaDoc tag: [" + type + "] " + apiName); 255 } 256 errCount++; 257 } 258 } 259 isPublicOrProtected(int modifier)260 private static boolean isPublicOrProtected(int modifier) { 261 return ((modifier & Modifier.PUBLIC) != 0) || ((modifier & Modifier.PROTECTED) != 0); 262 } 263 264 // Check if a method is automatically generated for a each Enum isBuiltinEnumMethod(Method mtd)265 private static boolean isBuiltinEnumMethod(Method mtd) { 266 // Just check method name for now 267 String name = mtd.getName(); 268 return name.equals("values") || name.equals("valueOf"); 269 } 270 isAPIDeprecated(APIInfo api)271 private static boolean isAPIDeprecated(APIInfo api) { 272 return api.isDeprecated() || api.isInternal() || api.isObsolete(); 273 } 274 findClassInfo(Set<APIInfo> apis, String cls)275 private static APIInfo findClassInfo(Set<APIInfo> apis, String cls) { 276 for (APIInfo api : apis) { 277 String clsName = api.getPackageName() + "." + api.getName(); 278 if (api.isClass() && clsName.equals(cls)) { 279 return api; 280 } 281 } 282 return null; 283 } 284 findFieldInfo(Set<APIInfo> apis, String cls, String field)285 private static APIInfo findFieldInfo(Set<APIInfo> apis, String cls, String field) { 286 for (APIInfo api : apis) { 287 String clsName = api.getPackageName() + "." + api.getClassName(); 288 if (api.isField() && clsName.equals(cls) && api.getName().equals(field)) { 289 return api; 290 } 291 } 292 return null; 293 } 294 findConstructorInfo(Set<APIInfo> apis, String cls, List<String> params)295 private static APIInfo findConstructorInfo(Set<APIInfo> apis, String cls, List<String> params) { 296 for (APIInfo api : apis) { 297 String clsName = api.getPackageName() + "." + api.getClassName(); 298 if (api.isConstructor() && clsName.equals(cls)) { 299 // check params 300 List<String> paramsFromApi = getParamNames(api); 301 if (paramsFromApi.size() == params.size()) { 302 boolean match = true; 303 for (int i = 0; i < params.size(); i++) { 304 if (!params.get(i).equals(paramsFromApi.get(i))) { 305 match = false; 306 break; 307 } 308 } 309 if (match) { 310 return api; 311 } 312 } 313 } 314 } 315 return null; 316 } 317 findMethodInfo(Set<APIInfo> apis, String cls, String method, List<String> params)318 private static APIInfo findMethodInfo(Set<APIInfo> apis, String cls, String method, List<String> params) { 319 for (APIInfo api : apis) { 320 String clsName = api.getPackageName() + "." + api.getClassName(); 321 if (api.isMethod() && clsName.equals(cls) && api.getName().equals(method)) { 322 // check params 323 List<String> paramsFromApi = getParamNames(api); 324 if (paramsFromApi.size() == params.size()) { 325 boolean match = true; 326 for (int i = 0; i < params.size(); i++) { 327 if (!params.get(i).equals(paramsFromApi.get(i))) { 328 match = false; 329 break; 330 } 331 } 332 if (match) { 333 return api; 334 } 335 } 336 } 337 } 338 return null; 339 } 340 findEnumInfo(Set<APIInfo> apis, String ecls)341 private static APIInfo findEnumInfo(Set<APIInfo> apis, String ecls) { 342 for (APIInfo api : apis) { 343 String clsName = api.getPackageName() + "." + api.getName(); 344 if (api.isEnum() && clsName.equals(ecls)) { 345 return api; 346 } 347 } 348 return null; 349 } 350 findEnumConstantInfo(Set<APIInfo> apis, String ecls, String econst)351 private static APIInfo findEnumConstantInfo(Set<APIInfo> apis, String ecls, String econst) { 352 for (APIInfo api : apis) { 353 String clsName = api.getPackageName() + "." + api.getClassName(); 354 if (api.isEnumConstant() && clsName.equals(ecls) && api.getName().equals(econst)) { 355 return api; 356 } 357 } 358 return null; 359 } 360 getParamNames(APIInfo api)361 private static List<String> getParamNames(APIInfo api) { 362 if (!api.isMethod() && !api.isConstructor()) { 363 throw new IllegalArgumentException(api.toString() + " is not a constructor or a method."); 364 } 365 366 List<String> nameList = new ArrayList<>(); 367 String signature = api.getSignature(); 368 int start = signature.indexOf('('); 369 int end = signature.indexOf(')'); 370 371 if (start < 0 || end < 0 || start > end) { 372 throw new RuntimeException(api.toString() + " has bad API signature: " + signature); 373 } 374 375 String paramsSegment = signature.substring(start + 1, end); 376 // erase generic args 377 if (paramsSegment.indexOf('<') >= 0) { 378 StringBuilder buf = new StringBuilder(); 379 int genericsNestLevel = 0; 380 for (int i = 0; i < paramsSegment.length(); i++) { 381 char c = paramsSegment.charAt(i); 382 if (genericsNestLevel > 0) { 383 if (c == '<') { 384 genericsNestLevel++; 385 } else if (c == '>') { 386 genericsNestLevel--; 387 } 388 } else { 389 if (c == '<') { 390 genericsNestLevel++; 391 } else { 392 buf.append(c); 393 } 394 } 395 } 396 paramsSegment = buf.toString(); 397 } 398 399 if (paramsSegment.length() > 0) { 400 String[] params = paramsSegment.split("\\s*,\\s*"); 401 for (String p : params) { 402 if (p.endsWith("...")) { 403 // varargs to array 404 p = p.substring(0, p.length() - 3) + "[]"; 405 } 406 nameList.add(p); 407 } 408 } 409 410 return nameList; 411 } 412 getParamNames(Constructor<?> ctor)413 private static List<String> getParamNames(Constructor<?> ctor) { 414 return toTypeNameList(ctor.getGenericParameterTypes()); 415 } 416 getParamNames(Method method)417 private static List<String> getParamNames(Method method) { 418 return toTypeNameList(method.getGenericParameterTypes()); 419 } 420 421 private static final String[] PRIMITIVES = { "byte", "short", "int", "long", "float", "double", "boolean", "char" }; 422 private static char[] PRIMITIVE_SIGNATURES = { 'B', 'S', 'I', 'J', 'F', 'D', 'Z', 'C' }; 423 toTypeNameList(Type[] types)424 private static List<String> toTypeNameList(Type[] types) { 425 List<String> nameList = new ArrayList<>(); 426 427 for (Type t : types) { 428 StringBuilder s = new StringBuilder(); 429 if (t instanceof ParameterizedType) { 430 // throw away generics parameters 431 ParameterizedType prdType = (ParameterizedType) t; 432 Class<?> rawType = (Class<?>) prdType.getRawType(); 433 s.append(rawType.getCanonicalName()); 434 } else if (t instanceof WildcardType) { 435 // we don't need to worry about WildcardType, 436 // because this tool erases generics parameters 437 // for comparing method/constructor parameters 438 throw new RuntimeException("WildcardType not supported by this tool"); 439 } else if (t instanceof TypeVariable) { 440 // this tool does not try to resolve actual parameter 441 // type - for example, "<T extends Object> void foo(T in)" 442 // this tool just use the type variable "T" for API signature 443 // comparison. This is actually not perfect, but should be 444 // sufficient for our purpose. 445 TypeVariable<?> tVar = (TypeVariable<?>) t; 446 s.append(tVar.getName()); 447 } else if (t instanceof GenericArrayType) { 448 // same as TypeVariable. "T[]" is sufficient enough. 449 GenericArrayType tGenArray = (GenericArrayType) t; 450 s.append(tGenArray.toString()); 451 } else if (t instanceof Class) { 452 Class<?> tClass = (Class<?>) t; 453 String tName = tClass.getCanonicalName(); 454 455 if (tName.charAt(0) == '[') { 456 // Array type 457 int idx = 0; 458 for (; idx < tName.length(); idx++) { 459 if (tName.charAt(idx) != '[') { 460 break; 461 } 462 } 463 int dimension = idx; 464 char sigChar = tName.charAt(dimension); 465 466 String elemType = null; 467 if (sigChar == 'L') { 468 // class 469 elemType = tName.substring(dimension + 1, tName.length() - 1); 470 } else { 471 // primitive 472 for (int i = 0; i < PRIMITIVE_SIGNATURES.length; i++) { 473 if (sigChar == PRIMITIVE_SIGNATURES[i]) { 474 elemType = PRIMITIVES[i]; 475 break; 476 } 477 } 478 } 479 480 if (elemType == null) { 481 throw new RuntimeException("Unexpected array type: " + tName); 482 } 483 484 s.append(elemType); 485 for (int i = 0; i < dimension; i++) { 486 s.append("[]"); 487 } 488 } else { 489 s.append(tName); 490 } 491 } else { 492 throw new IllegalArgumentException("Unknown type: " + t); 493 } 494 495 nameList.add(s.toString()); 496 } 497 498 return nameList; 499 } 500 formatParams(List<String> paramNames)501 private static String formatParams(List<String> paramNames) { 502 StringBuilder buf = new StringBuilder("("); 503 boolean isFirst = true; 504 for (String p : paramNames) { 505 if (isFirst) { 506 isFirst = false; 507 } else { 508 buf.append(", "); 509 } 510 buf.append(p); 511 } 512 buf.append(")"); 513 514 return buf.toString(); 515 } 516 } 517