1 /* 2 * Copyright (C) 2017 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 package android.signature.cts; 17 18 import android.signature.cts.JDiffClassDescription.JDiffConstructor; 19 import android.signature.cts.JDiffClassDescription.JDiffMethod; 20 import java.lang.annotation.Annotation; 21 import java.lang.reflect.AnnotatedElement; 22 import java.lang.reflect.Constructor; 23 import java.lang.reflect.Field; 24 import java.lang.reflect.GenericArrayType; 25 import java.lang.reflect.Member; 26 import java.lang.reflect.Method; 27 import java.lang.reflect.Modifier; 28 import java.lang.reflect.ParameterizedType; 29 import java.lang.reflect.Type; 30 import java.lang.reflect.TypeVariable; 31 import java.lang.reflect.WildcardType; 32 import java.util.ArrayList; 33 import java.util.HashSet; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 import java.util.regex.Matcher; 38 import java.util.regex.Pattern; 39 40 /** 41 * Uses reflection to obtain runtime representations of elements in the API. 42 */ 43 public class ReflectionHelper { 44 45 /** 46 * Finds the reflected class for the class under test. 47 * 48 * @param classDescription the description of the class to find. 49 * @return the reflected class, or null if not found. 50 */ findMatchingClass(JDiffClassDescription classDescription, ClassProvider classProvider)51 public static Class<?> findMatchingClass(JDiffClassDescription classDescription, 52 ClassProvider classProvider) throws ClassNotFoundException { 53 // even if there are no . in the string, split will return an 54 // array of length 1 55 String shortClassName = classDescription.getShortClassName(); 56 String[] classNameParts = shortClassName.split("\\."); 57 String packageName = classDescription.getPackageName(); 58 String outermostClassName = packageName + "." + classNameParts[0]; 59 int firstInnerClassNameIndex = 0; 60 61 return searchForClass(classProvider, classDescription.getAbsoluteClassName(), 62 outermostClassName, classNameParts, 63 firstInnerClassNameIndex); 64 } 65 searchForClass( ClassProvider classProvider, String absoluteClassName, String outermostClassName, String[] classNameParts, int outerClassNameIndex)66 private static Class<?> searchForClass( 67 ClassProvider classProvider, 68 String absoluteClassName, 69 String outermostClassName, String[] classNameParts, 70 int outerClassNameIndex) throws ClassNotFoundException { 71 72 Class<?> clz = classProvider.getClass(outermostClassName); 73 if (clz.getCanonicalName().equals(absoluteClassName)) { 74 return clz; 75 } 76 77 // Then it must be an inner class. 78 for (int x = outerClassNameIndex + 1; x < classNameParts.length; x++) { 79 clz = findInnerClassByName(clz, classNameParts[x]); 80 if (clz == null) { 81 return null; 82 } 83 if (clz.getCanonicalName().equals(absoluteClassName)) { 84 return clz; 85 } 86 } 87 return null; 88 } 89 findMatchingClass(String absoluteClassName, ClassProvider classProvider)90 static Class<?> findMatchingClass(String absoluteClassName, ClassProvider classProvider) 91 throws ClassNotFoundException { 92 93 String[] classNameParts = absoluteClassName.split("\\."); 94 StringBuilder builder = new StringBuilder(); 95 String separator = ""; 96 int start; 97 for (start = 0; start < classNameParts.length; start++) { 98 String classNamePart = classNameParts[start]; 99 builder.append(separator).append(classNamePart); 100 separator = "."; 101 if (Character.isUpperCase(classNamePart.charAt(0))) { 102 break; 103 } 104 } 105 String outermostClassName = builder.toString(); 106 107 return searchForClass(classProvider, absoluteClassName, outermostClassName, classNameParts, 108 start); 109 } 110 111 /** 112 * Searches the class for the specified inner class. 113 * 114 * @param clz the class to search in. 115 * @param simpleName the simpleName of the class to find 116 * @return the class being searched for, or null if it can't be found. 117 */ findInnerClassByName(Class<?> clz, String simpleName)118 private static Class<?> findInnerClassByName(Class<?> clz, String simpleName) { 119 for (Class<?> c : clz.getDeclaredClasses()) { 120 if (c.getSimpleName().equals(simpleName)) { 121 return c; 122 } 123 } 124 return null; 125 } 126 127 /** 128 * Searches available constructor. 129 * 130 * @param runtimeClass the class in which to search. 131 * @param jdiffDes constructor description to find. 132 * @param mismatchReasons a map from rejected constructor to the reason it was rejected. 133 * @return reflected constructor, or null if not found. 134 */ findMatchingConstructor(Class<?> runtimeClass, JDiffConstructor jdiffDes, Map<Constructor, String> mismatchReasons)135 static Constructor<?> findMatchingConstructor(Class<?> runtimeClass, 136 JDiffConstructor jdiffDes, Map<Constructor, String> mismatchReasons) { 137 138 for (Constructor<?> c : runtimeClass.getDeclaredConstructors()) { 139 Type[] params = c.getGenericParameterTypes(); 140 boolean isStaticClass = ((runtimeClass.getModifiers() & Modifier.STATIC) != 0); 141 142 int startParamOffset = 0; 143 int numberOfParams = params.length; 144 145 // non-static inner class -> skip implicit parent pointer 146 // as first arg 147 if (runtimeClass.isMemberClass() && !isStaticClass && params.length >= 1) { 148 startParamOffset = 1; 149 --numberOfParams; 150 } 151 152 ArrayList<String> jdiffParamList = jdiffDes.mParamList; 153 if (jdiffParamList.size() == numberOfParams) { 154 boolean isFound = true; 155 // i counts jdiff params, j counts reflected params 156 int i = 0; 157 int j = startParamOffset; 158 while (i < jdiffParamList.size()) { 159 String expectedParameter = jdiffParamList.get(i); 160 Type actualParameter = params[j]; 161 if (!compareParam(expectedParameter, actualParameter, 162 DefaultTypeComparator.INSTANCE)) { 163 mismatchReasons.put(c, 164 String.format("parameter %d mismatch: expected (%s), found (%s)", 165 i, 166 expectedParameter, 167 actualParameter)); 168 isFound = false; 169 break; 170 } 171 ++i; 172 ++j; 173 } 174 if (isFound) { 175 return c; 176 } 177 } else { 178 mismatchReasons.put(c, 179 String.format("parameter list length mismatch: expected %d, found %d", 180 jdiffParamList.size(), 181 params.length)); 182 } 183 } 184 return null; 185 } 186 187 /** 188 * Compares the parameter from the API and the parameter from 189 * reflection. 190 * 191 * @param jdiffParam param parsed from the API xml file. 192 * @param reflectionParamType param gotten from the Java reflection. 193 * @param typeComparator compares two types to determine if they are equal. 194 * @return True if the two params match, otherwise return false. 195 */ compareParam(String jdiffParam, Type reflectionParamType, TypeComparator typeComparator)196 private static boolean compareParam(String jdiffParam, Type reflectionParamType, 197 TypeComparator typeComparator) { 198 if (jdiffParam == null) { 199 return false; 200 } 201 202 String reflectionParam = typeToString(reflectionParamType); 203 // Most things aren't varargs, so just do a simple compare 204 // first. 205 if (typeComparator.compare(jdiffParam, reflectionParam)) { 206 return true; 207 } 208 209 // Check for varargs. jdiff reports varargs as ..., while 210 // reflection reports them as [] 211 int jdiffParamEndOffset = jdiffParam.indexOf("..."); 212 int reflectionParamEndOffset = reflectionParam != null 213 ? reflectionParam.lastIndexOf("[]") : -1; 214 if (jdiffParamEndOffset != -1 && reflectionParamEndOffset != -1) { 215 jdiffParam = jdiffParam.substring(0, jdiffParamEndOffset); 216 reflectionParam = reflectionParam.substring(0, reflectionParamEndOffset); 217 return typeComparator.compare(jdiffParam, reflectionParam); 218 } 219 220 return false; 221 } 222 223 /** 224 * Finds the reflected method specified by the method description. 225 * 226 * @param runtimeClass the class in which to search. 227 * @param method description of the method to find 228 * @param mismatchReasons a map from rejected method to the reason it was rejected, only 229 * contains methods with the same name. 230 * @return the reflected method, or null if not found. 231 */ findMatchingMethod( Class<?> runtimeClass, JDiffMethod method, Map<Method, String> mismatchReasons)232 static Method findMatchingMethod( 233 Class<?> runtimeClass, JDiffMethod method, Map<Method, String> mismatchReasons) { 234 235 // Search through the class to find the methods just in case the method was actually 236 // declared in a superclass which is not part of the API and so was made to appear as if 237 // it was declared in each of the hidden class' subclasses. Cannot use getMethods() as that 238 // will only return public methods and the API includes protected methods. 239 Class<?> currentClass = runtimeClass; 240 while (currentClass != null) { 241 Method[] reflectedMethods = currentClass.getDeclaredMethods(); 242 243 for (Method reflectedMethod : reflectedMethods) { 244 // If the method names aren't equal, the methods can't match. 245 if (!method.mName.equals(reflectedMethod.getName())) { 246 continue; 247 } 248 249 if (matchesSignature(method, reflectedMethod, mismatchReasons)) { 250 return reflectedMethod; 251 } 252 } 253 254 currentClass = currentClass.getSuperclass(); 255 } 256 257 return null; 258 } 259 260 /** 261 * Checks if the two types of methods are the same. 262 * 263 * @param jDiffMethod the jDiffMethod to compare 264 * @param reflectedMethod the reflected method to compare 265 * @param mismatchReasons map from method to reason it did not match, used when reporting 266 * missing methods. 267 * @return true, if both methods are the same 268 */ matchesSignature(JDiffMethod jDiffMethod, Method reflectedMethod, Map<Method, String> mismatchReasons)269 static boolean matchesSignature(JDiffMethod jDiffMethod, Method reflectedMethod, 270 Map<Method, String> mismatchReasons) { 271 // If the method is a bridge then use a special comparator for comparing types as 272 // bridge methods created for generic methods may not have generic signatures. 273 // See b/123558763 for more information. 274 TypeComparator typeComparator = reflectedMethod.isBridge() 275 ? BridgeTypeComparator.INSTANCE : DefaultTypeComparator.INSTANCE; 276 277 String jdiffReturnType = jDiffMethod.mReturnType; 278 String reflectionReturnType = typeToString(reflectedMethod.getGenericReturnType()); 279 280 // Next, compare the return types of the two methods. If 281 // they aren't equal, the methods can't match. 282 if (!typeComparator.compare(jdiffReturnType, reflectionReturnType)) { 283 mismatchReasons.put(reflectedMethod, 284 String.format("return type mismatch: expected %s, found %s", jdiffReturnType, 285 reflectionReturnType)); 286 return false; 287 } 288 289 List<String> jdiffParamList = jDiffMethod.mParamList; 290 Type[] params = reflectedMethod.getGenericParameterTypes(); 291 292 // Next, check the method parameters. If they have different 293 // parameter lengths, the two methods can't match. 294 if (jdiffParamList.size() != params.length) { 295 mismatchReasons.put(reflectedMethod, 296 String.format("parameter list length mismatch: expected %s, found %s", 297 jdiffParamList.size(), 298 params.length)); 299 return false; 300 } 301 302 boolean piecewiseParamsMatch = true; 303 304 // Compare method parameters piecewise and return true if they all match. 305 for (int i = 0; i < jdiffParamList.size(); i++) { 306 piecewiseParamsMatch &= compareParam(jdiffParamList.get(i), params[i], typeComparator); 307 } 308 if (piecewiseParamsMatch) { 309 return true; 310 } 311 312 /* NOTE: There are cases where piecewise method parameter checking 313 * fails even though the strings are equal, so compare entire strings 314 * against each other. This is not done by default to avoid a 315 * TransactionTooLargeException. 316 * Additionally, this can fail anyway due to extra 317 * information dug up by reflection. 318 * 319 * TODO: fix parameter equality checking and reflection matching 320 * See https://b.corp.google.com/issues/27726349 321 */ 322 323 StringBuilder reflectedMethodParams = new StringBuilder(""); 324 StringBuilder jdiffMethodParams = new StringBuilder(""); 325 326 String sep = ""; 327 for (int i = 0; i < jdiffParamList.size(); i++) { 328 jdiffMethodParams.append(sep).append(jdiffParamList.get(i)); 329 reflectedMethodParams.append(sep).append(params[i].getTypeName()); 330 sep = ", "; 331 } 332 333 String jDiffFName = jdiffMethodParams.toString(); 334 String refName = reflectedMethodParams.toString(); 335 336 boolean signatureMatches = jDiffFName.equals(refName); 337 if (!signatureMatches) { 338 mismatchReasons.put(reflectedMethod, 339 String.format("parameter signature mismatch: expected (%s), found (%s)", 340 jDiffFName, 341 refName)); 342 } 343 344 return signatureMatches; 345 } 346 347 /** 348 * Converts WildcardType array into a jdiff compatible string.. 349 * This is a helper function for typeToString. 350 * 351 * @param types array of types to format. 352 * @return the jdiff formatted string. 353 */ concatWildcardTypes(Type[] types)354 private static String concatWildcardTypes(Type[] types) { 355 StringBuilder sb = new StringBuilder(); 356 int elementNum = 0; 357 for (Type t : types) { 358 sb.append(typeToString(t)); 359 if (++elementNum < types.length) { 360 sb.append(" & "); 361 } 362 } 363 return sb.toString(); 364 } 365 366 /** 367 * Converts a Type into a jdiff compatible String. The returned 368 * types from this function should match the same Strings that 369 * jdiff is providing to us. 370 * 371 * @param type the type to convert. 372 * @return the jdiff formatted string. 373 */ typeToString(Type type)374 public static String typeToString(Type type) { 375 if (type instanceof ParameterizedType) { 376 ParameterizedType pt = (ParameterizedType) type; 377 378 StringBuilder sb = new StringBuilder(); 379 sb.append(typeToString(pt.getRawType())); 380 sb.append("<"); 381 382 int elementNum = 0; 383 Type[] types = pt.getActualTypeArguments(); 384 for (Type t : types) { 385 sb.append(typeToString(t)); 386 if (++elementNum < types.length) { 387 // Must match separator used in 388 // android.signature.cts.KtHelper.toDefaultTypeString. 389 sb.append(","); 390 } 391 } 392 393 sb.append(">"); 394 return sb.toString(); 395 } else if (type instanceof TypeVariable) { 396 return ((TypeVariable<?>) type).getName(); 397 } else if (type instanceof Class) { 398 return ((Class<?>) type).getCanonicalName(); 399 } else if (type instanceof GenericArrayType) { 400 String typeName = typeToString(((GenericArrayType) type).getGenericComponentType()); 401 return typeName + "[]"; 402 } else if (type instanceof WildcardType) { 403 WildcardType wt = (WildcardType) type; 404 Type[] lowerBounds = wt.getLowerBounds(); 405 if (lowerBounds.length == 0) { 406 String name = "? extends " + concatWildcardTypes(wt.getUpperBounds()); 407 408 // Special case for ? 409 if (name.equals("? extends java.lang.Object")) { 410 return "?"; 411 } else { 412 return name; 413 } 414 } else { 415 String name = concatWildcardTypes(wt.getUpperBounds()) + 416 " super " + 417 concatWildcardTypes(wt.getLowerBounds()); 418 // Another special case for ? 419 name = name.replace("java.lang.Object", "?"); 420 return name; 421 } 422 } else { 423 throw new RuntimeException("Got an unknown java.lang.Type"); 424 } 425 } 426 427 private final static Pattern REPEATING_ANNOTATION_PATTERN = 428 Pattern.compile("@.*\\(value=\\[(.*)\\]\\)"); 429 hasMatchingAnnotation(AnnotatedElement elem, String annotationSpec)430 public static boolean hasMatchingAnnotation(AnnotatedElement elem, String annotationSpec) { 431 for (Annotation a : elem.getAnnotations()) { 432 if (a.toString().equals(annotationSpec)) { 433 return true; 434 } 435 // It could be a repeating annotation. In that case, a.toString() returns 436 // "@MyAnnotation$Container(value=[@MyAnnotation(A), @MyAnnotation(B)])" 437 // Then, iterate over @MyAnnotation(A) and @MyAnnotation(B). 438 Matcher m = REPEATING_ANNOTATION_PATTERN.matcher(a.toString()); 439 if (m.matches()) { 440 for (String token : m.group(1).split(", ")) { 441 if (token.equals(annotationSpec)) { 442 return true; 443 } 444 } 445 } 446 } 447 return false; 448 } 449 450 /** 451 * Returns a list of constructors which are annotated with the given annotation class. 452 */ getAnnotatedConstructors(Class<?> clazz, String annotationSpec)453 public static Set<Constructor<?>> getAnnotatedConstructors(Class<?> clazz, 454 String annotationSpec) { 455 Set<Constructor<?>> result = new HashSet<>(); 456 if (annotationSpec != null) { 457 for (Constructor<?> c : clazz.getDeclaredConstructors()) { 458 if (hasMatchingAnnotation(c, annotationSpec)) { 459 // TODO(b/71630695): currently, some API members are not annotated, because 460 // a member is automatically added to the API set if it is in a class with 461 // annotation and it is not @hide. <member>.getDeclaringClass(). 462 // isAnnotationPresent(annotationClass) won't help because it will then 463 // incorrectly include non-API members which are marked as @hide; 464 // @hide isn't visible at runtime. Until the issue is fixed, we should 465 // omit those automatically added API members from the test. 466 result.add(c); 467 } 468 } 469 } 470 return result; 471 } 472 473 /** 474 * Returns a list of methods which are annotated with the given annotation class. 475 */ getAnnotatedMethods(Class<?> clazz, String annotationSpec)476 public static Set<Method> getAnnotatedMethods(Class<?> clazz, String annotationSpec) { 477 Set<Method> result = new HashSet<>(); 478 if (annotationSpec != null) { 479 for (Method m : clazz.getDeclaredMethods()) { 480 if (hasMatchingAnnotation(m, annotationSpec)) { 481 // TODO(b/71630695): see getAnnotatedConstructors for details 482 result.add(m); 483 } 484 } 485 } 486 return result; 487 } 488 489 /** 490 * Returns a list of fields which are annotated with the given annotation class. 491 */ getAnnotatedFields(Class<?> clazz, String annotationSpec)492 public static Set<Field> getAnnotatedFields(Class<?> clazz, String annotationSpec) { 493 Set<Field> result = new HashSet<>(); 494 if (annotationSpec != null) { 495 for (Field f : clazz.getDeclaredFields()) { 496 if (hasMatchingAnnotation(f, annotationSpec)) { 497 // TODO(b/71630695): see getAnnotatedConstructors for details 498 result.add(f); 499 } 500 } 501 } 502 return result; 503 } 504 isInAnnotatedClass(Member m, String annotationSpec)505 private static boolean isInAnnotatedClass(Member m, String annotationSpec) { 506 Class<?> clazz = m.getDeclaringClass(); 507 do { 508 if (hasMatchingAnnotation(clazz, annotationSpec)) { 509 return true; 510 } 511 } while ((clazz = clazz.getDeclaringClass()) != null); 512 return false; 513 } 514 isAnnotatedOrInAnnotatedClass(Field field, String annotationSpec)515 public static boolean isAnnotatedOrInAnnotatedClass(Field field, String annotationSpec) { 516 if (annotationSpec == null) { 517 return true; 518 } 519 return hasMatchingAnnotation(field, annotationSpec) 520 || isInAnnotatedClass(field, annotationSpec); 521 } 522 isAnnotatedOrInAnnotatedClass(Constructor<?> constructor, String annotationSpec)523 public static boolean isAnnotatedOrInAnnotatedClass(Constructor<?> constructor, 524 String annotationSpec) { 525 if (annotationSpec == null) { 526 return true; 527 } 528 return hasMatchingAnnotation(constructor, annotationSpec) 529 || isInAnnotatedClass(constructor, annotationSpec); 530 } 531 isAnnotatedOrInAnnotatedClass(Method method, String annotationSpec)532 public static boolean isAnnotatedOrInAnnotatedClass(Method method, String annotationSpec) { 533 if (annotationSpec == null) { 534 return true; 535 } 536 return hasMatchingAnnotation(method, annotationSpec) 537 || isInAnnotatedClass(method, annotationSpec); 538 } 539 isOverridingAnnotatedMethod(Method method, String annotationSpec)540 public static boolean isOverridingAnnotatedMethod(Method method, String annotationSpec) { 541 Class<?> clazz = method.getDeclaringClass(); 542 while (!(clazz = clazz.getSuperclass()).equals(Object.class)) { 543 try { 544 Method overriddenMethod; 545 overriddenMethod = clazz.getDeclaredMethod(method.getName(), 546 method.getParameterTypes()); 547 if (overriddenMethod != null) { 548 return isAnnotatedOrInAnnotatedClass(overriddenMethod, annotationSpec); 549 } 550 } catch (NoSuchMethodException e) { 551 continue; 552 } catch (SecurityException e) { 553 throw new RuntimeException( 554 "Error while searching for overridden method. " + method.toString(), e); 555 } 556 } 557 return false; 558 } 559 findRequiredClass(JDiffClassDescription classDescription, ClassProvider classProvider)560 static Class<?> findRequiredClass(JDiffClassDescription classDescription, 561 ClassProvider classProvider) { 562 try { 563 return findMatchingClass(classDescription, classProvider); 564 } catch (ClassNotFoundException e) { 565 LogHelper.loge("ClassNotFoundException for " + classDescription.getAbsoluteClassName(), e); 566 return null; 567 } 568 } 569 570 /** 571 * Compare the string representation of types for equality. 572 */ 573 interface TypeComparator { compare(String apiType, String reflectedType)574 boolean compare(String apiType, String reflectedType); 575 } 576 577 /** 578 * Compare the types using their default signature, i.e. generic for generic methods, otherwise 579 * basic types. 580 */ 581 static class DefaultTypeComparator implements TypeComparator { 582 static final TypeComparator INSTANCE = new DefaultTypeComparator(); 583 @Override compare(String apiType, String reflectedType)584 public boolean compare(String apiType, String reflectedType) { 585 return apiType.equals(reflectedType); 586 } 587 } 588 589 /** 590 * Comparator for the types of bridge methods. 591 * 592 * <p>Bridge methods may not have generic signatures so compare as for 593 * {@link DefaultTypeComparator}, but if they do not match and the api type is 594 * generic then fall back to comparing their raw types. 595 */ 596 static class BridgeTypeComparator implements TypeComparator { 597 static final TypeComparator INSTANCE = new BridgeTypeComparator(); 598 @Override compare(String apiType, String reflectedType)599 public boolean compare(String apiType, String reflectedType) { 600 if (DefaultTypeComparator.INSTANCE.compare(apiType, reflectedType)) { 601 return true; 602 } 603 604 // If the method is a bridge method and the return types are generic then compare the 605 // non generic types as bridge methods do not have generic types. 606 int index = apiType.indexOf('<'); 607 if (index != -1) { 608 String rawReturnType = apiType.substring(0, index); 609 return rawReturnType.equals(reflectedType); 610 } 611 return false; 612 } 613 } 614 } 615