blob: 50ba0aca91762110d9c2dd61fc2078de5d259203 [file] [log] [blame]
/*
*******************************************************************************
* Copyright (C) 2014, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*/
package com.ibm.icu.dev.tool.docs;
import java.io.File;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
public class DeprecatedAPIChecker {
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Illegal command argument. Specify the API signature file path.");
}
// Load the ICU4J API signature file
Set<APIInfo> apiInfoSet = APIData.read(new File(args[0]), true).getAPIInfoSet();
DeprecatedAPIChecker checker = new DeprecatedAPIChecker(apiInfoSet, new PrintWriter(System.err, true));
checker.checkDeprecated();
System.exit(checker.errCount);
}
private int errCount = 0;
private Set<APIInfo> apiInfoSet;
private PrintWriter pw;
public DeprecatedAPIChecker(Set<APIInfo> apiInfoSet, PrintWriter pw) {
this.apiInfoSet = apiInfoSet;
this.pw = pw;
}
public int errorCount() {
return errCount;
}
public void checkDeprecated() {
// Gather API class/enum names and its names that can be
// used for Class.forName()
Map<String, String> apiClassNameMap = new TreeMap<String, String>();
for (APIInfo api : apiInfoSet) {
if (!api.isPublic() && !api.isProtected()) {
continue;
}
if (!api.isClass() && !api.isEnum()) {
continue;
}
String packageName = api.getPackageName();
String className = api.getName();
// Replacing separator for nested class/enum (replacing '.' with
// '$'), so we can use the name for Class.forName(String)
String classNamePath = className.contains(".") ? className.replace('.', '$') : className;
apiClassNameMap.put(packageName + "." + classNamePath, packageName + "." + className);
}
// Walk through API classes using reflection
for (Entry<String, String> classEntry : apiClassNameMap.entrySet()) {
String classNamePath = classEntry.getKey();
try {
Class<?> cls = Class.forName(classNamePath);
if (cls.isEnum()) {
checkEnum(cls, apiClassNameMap);
} else {
checkClass(cls, apiClassNameMap);
}
} catch (ClassNotFoundException e) {
pw.println("## Error ## Class " + classNamePath + " is not found.");
errCount++;
}
}
}
private void checkClass(Class<?> cls, Map<String, String> clsNameMap) {
assert !cls.isEnum();
String clsPath = cls.getName();
String clsName = clsNameMap.get(clsPath);
APIInfo api = null;
if (clsName != null) {
api = findClassInfo(apiInfoSet, clsName);
}
if (api == null) {
pw.println("## Error ## Class " + clsName + " is not found in the API signature data.");
errCount++;
}
// check class
compareDeprecated(isAPIDeprecated(api), cls.isAnnotationPresent(Deprecated.class), clsName, null, "Class");
// check fields
for (Field f : cls.getDeclaredFields()) {
if (!isPublicOrProtected(f.getModifiers())) {
continue;
}
String fName = f.getName();
api = findFieldInfo(apiInfoSet, clsName, fName);
if (api == null) {
pw.println("## Error ## Field " + clsName + "." + fName + " is not found in the API signature data.");
errCount++;
continue;
}
compareDeprecated(isAPIDeprecated(api), f.isAnnotationPresent(Deprecated.class), clsName, fName, "Field");
}
// check constructors
for (Constructor<?> ctor : cls.getDeclaredConstructors()) {
if (!isPublicOrProtected(ctor.getModifiers())) {
continue;
}
List<String> paramNames = getParamNames(ctor);
api = findConstructorInfo(apiInfoSet, clsName, paramNames);
if (api == null) {
pw.println("## Error ## Constructor " + clsName + formatParams(paramNames)
+ " is not found in the API signature data.");
errCount++;
continue;
}
compareDeprecated(isAPIDeprecated(api), ctor.isAnnotationPresent(Deprecated.class), clsName,
api.getClassName() + formatParams(paramNames), "Constructor");
}
// check methods
for (Method mtd : cls.getDeclaredMethods()) {
// Note: We exclude synthetic method.
if (!isPublicOrProtected(mtd.getModifiers()) || mtd.isSynthetic()) {
continue;
}
String mtdName = mtd.getName();
List<String> paramNames = getParamNames(mtd);
api = findMethodInfo(apiInfoSet, clsName, mtdName, paramNames);
if (api == null) {
pw.println("## Error ## Method " + clsName + "#" + mtdName + formatParams(paramNames)
+ " is not found in the API signature data.");
errCount++;
continue;
}
compareDeprecated(isAPIDeprecated(api), mtd.isAnnotationPresent(Deprecated.class), clsName, mtdName
+ formatParams(paramNames), "Method");
}
}
private void checkEnum(Class<?> cls, Map<String, String> clsNameMap) {
assert cls.isEnum();
String enumPath = cls.getName();
String enumName = clsNameMap.get(enumPath);
APIInfo api = null;
if (enumName != null) {
api = findEnumInfo(apiInfoSet, enumName);
}
if (api == null) {
pw.println("## Error ## Enum " + enumName + " is not found in the API signature data.");
errCount++;
}
// check enum
compareDeprecated(isAPIDeprecated(api), cls.isAnnotationPresent(Deprecated.class), enumName, null, "Enum");
// check enum constants
for (Field ec : cls.getDeclaredFields()) {
if (!ec.isEnumConstant()) {
continue;
}
String ecName = ec.getName();
api = findEnumConstantInfo(apiInfoSet, enumName, ecName);
if (api == null) {
pw.println("## Error ## Enum constant " + enumName + "." + ecName
+ " is not found in the API signature data.");
errCount++;
continue;
}
compareDeprecated(isAPIDeprecated(api), ec.isAnnotationPresent(Deprecated.class), enumName, ecName,
"Enum Constant");
}
// check methods
for (Method mtd : cls.getDeclaredMethods()) {
// Note: We exclude built-in methods in a Java Enum instance
if (!isPublicOrProtected(mtd.getModifiers()) || isBuiltinEnumMethod(mtd)) {
continue;
}
String mtdName = mtd.getName();
List<String> paramNames = getParamNames(mtd);
api = findMethodInfo(apiInfoSet, enumName, mtdName, paramNames);
if (api == null) {
pw.println("## Error ## Method " + enumName + "#" + mtdName + formatParams(paramNames)
+ " is not found in the API signature data.");
errCount++;
continue;
}
compareDeprecated(isAPIDeprecated(api), mtd.isAnnotationPresent(Deprecated.class), enumName, mtdName
+ formatParams(paramNames), "Method");
}
}
private void compareDeprecated(boolean depTag, boolean depAnt, String cls, String name, String type) {
if (depTag != depAnt) {
String apiName = cls;
if (name != null) {
apiName += "." + name;
}
if (depTag) {
pw.println("No @Deprecated annotation: [" + type + "] " + apiName);
} else {
pw.println("No @deprecated JavaDoc tag: [" + type + "] " + apiName);
}
errCount++;
}
}
private static boolean isPublicOrProtected(int modifier) {
return ((modifier & Modifier.PUBLIC) != 0) || ((modifier & Modifier.PROTECTED) != 0);
}
// Check if a method is automatically generated for a each Enum
private static boolean isBuiltinEnumMethod(Method mtd) {
// Just check method name for now
String name = mtd.getName();
return name.equals("values") || name.equals("valueOf");
}
private static boolean isAPIDeprecated(APIInfo api) {
return api.isDeprecated() || api.isInternal() || api.isObsolete();
}
private static APIInfo findClassInfo(Set<APIInfo> apis, String cls) {
for (APIInfo api : apis) {
String clsName = api.getPackageName() + "." + api.getName();
if (api.isClass() && clsName.equals(cls)) {
return api;
}
}
return null;
}
private static APIInfo findFieldInfo(Set<APIInfo> apis, String cls, String field) {
for (APIInfo api : apis) {
String clsName = api.getPackageName() + "." + api.getClassName();
if (api.isField() && clsName.equals(cls) && api.getName().equals(field)) {
return api;
}
}
return null;
}
private static APIInfo findConstructorInfo(Set<APIInfo> apis, String cls, List<String> params) {
for (APIInfo api : apis) {
String clsName = api.getPackageName() + "." + api.getClassName();
if (api.isConstructor() && clsName.equals(cls)) {
// check params
List<String> paramsFromApi = getParamNames(api);
if (paramsFromApi.size() == params.size()) {
boolean match = true;
for (int i = 0; i < params.size(); i++) {
if (!params.get(i).equals(paramsFromApi.get(i))) {
match = false;
break;
}
}
if (match) {
return api;
}
}
}
}
return null;
}
private static APIInfo findMethodInfo(Set<APIInfo> apis, String cls, String method, List<String> params) {
for (APIInfo api : apis) {
String clsName = api.getPackageName() + "." + api.getClassName();
if (api.isMethod() && clsName.equals(cls) && api.getName().equals(method)) {
// check params
List<String> paramsFromApi = getParamNames(api);
if (paramsFromApi.size() == params.size()) {
boolean match = true;
for (int i = 0; i < params.size(); i++) {
if (!params.get(i).equals(paramsFromApi.get(i))) {
match = false;
break;
}
}
if (match) {
return api;
}
}
}
}
return null;
}
private static APIInfo findEnumInfo(Set<APIInfo> apis, String ecls) {
for (APIInfo api : apis) {
String clsName = api.getPackageName() + "." + api.getName();
if (api.isEnum() && clsName.equals(ecls)) {
return api;
}
}
return null;
}
private static APIInfo findEnumConstantInfo(Set<APIInfo> apis, String ecls, String econst) {
for (APIInfo api : apis) {
String clsName = api.getPackageName() + "." + api.getClassName();
if (api.isEnumConstant() && clsName.equals(ecls) && api.getName().equals(econst)) {
return api;
}
}
return null;
}
private static List<String> getParamNames(APIInfo api) {
if (!api.isMethod() && !api.isConstructor()) {
throw new IllegalArgumentException(api.toString() + " is not a constructor or a method.");
}
List<String> nameList = new ArrayList<String>();
String signature = api.getSignature();
int start = signature.indexOf('(');
int end = signature.indexOf(')');
if (start < 0 || end < 0 || start > end) {
throw new RuntimeException(api.toString() + " has bad API signature: " + signature);
}
String paramsSegment = signature.substring(start + 1, end);
// erase generic args
if (paramsSegment.indexOf('<') >= 0) {
StringBuilder buf = new StringBuilder();
boolean inGenericsParams = false;
for (int i = 0; i < paramsSegment.length(); i++) {
char c = paramsSegment.charAt(i);
if (inGenericsParams) {
if (c == '>') {
inGenericsParams = false;
}
} else {
if (c == '<') {
inGenericsParams = true;
} else {
buf.append(c);
}
}
}
paramsSegment = buf.toString();
}
if (paramsSegment.length() > 0) {
String[] params = paramsSegment.split("\\s*,\\s*");
for (String p : params) {
if (p.endsWith("...")) {
// varargs to array
p = p.substring(0, p.length() - 3) + "[]";
}
nameList.add(p);
}
}
return nameList;
}
private static List<String> getParamNames(Constructor<?> ctor) {
return toTypeNameList(ctor.getGenericParameterTypes());
}
private static List<String> getParamNames(Method method) {
return toTypeNameList(method.getGenericParameterTypes());
}
private static final String[] PRIMITIVES = { "byte", "short", "int", "long", "float", "double", "boolean", "char" };
private static char[] PRIMITIVE_SIGNATURES = { 'B', 'S', 'I', 'J', 'F', 'D', 'Z', 'C' };
private static List<String> toTypeNameList(Type[] types) {
List<String> nameList = new ArrayList<String>();
for (Type t : types) {
StringBuilder s = new StringBuilder();
if (t instanceof ParameterizedType) {
// throw away generics parameters
ParameterizedType prdType = (ParameterizedType) t;
Class<?> rawType = (Class<?>) prdType.getRawType();
s.append(rawType.getCanonicalName());
} else if (t instanceof WildcardType) {
// we don't need to worry about WildcardType,
// because this tool erases generics parameters
// for comparing method/constructor parameters
throw new RuntimeException("WildcardType not supported by this tool");
} else if (t instanceof TypeVariable) {
// this tool does not try to resolve actual parameter
// type - for example, "<T extends Object> void foo(T in)"
// this tool just use the type variable "T" for API signature
// comparison. This is actually not perfect, but should be
// sufficient for our purpose.
TypeVariable<?> tVar = (TypeVariable<?>) t;
s.append(tVar.getName());
} else if (t instanceof GenericArrayType) {
// same as TypeVariable. "T[]" is sufficient enough.
GenericArrayType tGenArray = (GenericArrayType) t;
s.append(tGenArray.toString());
} else if (t instanceof Class) {
Class<?> tClass = (Class<?>) t;
String tName = tClass.getCanonicalName();
if (tName.charAt(0) == '[') {
// Array type
int idx = 0;
for (; idx < tName.length(); idx++) {
if (tName.charAt(idx) != '[') {
break;
}
}
int dimension = idx;
char sigChar = tName.charAt(dimension);
String elemType = null;
if (sigChar == 'L') {
// class
elemType = tName.substring(dimension + 1, tName.length() - 1);
} else {
// primitive
for (int i = 0; i < PRIMITIVE_SIGNATURES.length; i++) {
if (sigChar == PRIMITIVE_SIGNATURES[i]) {
elemType = PRIMITIVES[i];
break;
}
}
}
if (elemType == null) {
throw new RuntimeException("Unexpected array type: " + tName);
}
s.append(elemType);
for (int i = 0; i < dimension; i++) {
s.append("[]");
}
} else {
s.append(tName);
}
} else {
throw new IllegalArgumentException("Unknown type: " + t);
}
nameList.add(s.toString());
}
return nameList;
}
private static String formatParams(List<String> paramNames) {
StringBuilder buf = new StringBuilder("(");
boolean isFirst = true;
for (String p : paramNames) {
if (isFirst) {
isFirst = false;
} else {
buf.append(", ");
}
buf.append(p);
}
buf.append(")");
return buf.toString();
}
}