1 /* 2 * Copyright (C) 2018. Uber Technologies 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 com.uber.nullaway.jarinfer; 17 18 import com.google.common.base.Preconditions; 19 import com.google.common.collect.ImmutableMap; 20 import com.google.common.collect.ImmutableSet; 21 import com.ibm.wala.cfg.ControlFlowGraph; 22 import com.ibm.wala.classLoader.CodeScanner; 23 import com.ibm.wala.classLoader.IClass; 24 import com.ibm.wala.classLoader.IClassLoader; 25 import com.ibm.wala.classLoader.IMethod; 26 import com.ibm.wala.classLoader.PhantomClass; 27 import com.ibm.wala.classLoader.ShrikeCTMethod; 28 import com.ibm.wala.core.util.config.AnalysisScopeReader; 29 import com.ibm.wala.core.util.warnings.Warnings; 30 import com.ibm.wala.ipa.callgraph.AnalysisCache; 31 import com.ibm.wala.ipa.callgraph.AnalysisCacheImpl; 32 import com.ibm.wala.ipa.callgraph.AnalysisOptions; 33 import com.ibm.wala.ipa.callgraph.AnalysisScope; 34 import com.ibm.wala.ipa.callgraph.impl.Everywhere; 35 import com.ibm.wala.ipa.cha.ClassHierarchyException; 36 import com.ibm.wala.ipa.cha.ClassHierarchyFactory; 37 import com.ibm.wala.ipa.cha.IClassHierarchy; 38 import com.ibm.wala.shrike.shrikeCT.InvalidClassFileException; 39 import com.ibm.wala.ssa.IR; 40 import com.ibm.wala.ssa.ISSABasicBlock; 41 import com.ibm.wala.ssa.SSAInstruction; 42 import com.ibm.wala.types.ClassLoaderReference; 43 import com.ibm.wala.types.TypeReference; 44 import com.ibm.wala.util.collections.Iterator2Iterable; 45 import com.ibm.wala.util.config.FileOfClasses; 46 import java.io.ByteArrayInputStream; 47 import java.io.DataOutputStream; 48 import java.io.File; 49 import java.io.FileInputStream; 50 import java.io.FileOutputStream; 51 import java.io.IOException; 52 import java.io.InputStream; 53 import java.io.OutputStream; 54 import java.nio.charset.StandardCharsets; 55 import java.nio.file.Files; 56 import java.nio.file.Paths; 57 import java.nio.file.attribute.FileTime; 58 import java.util.Arrays; 59 import java.util.HashMap; 60 import java.util.HashSet; 61 import java.util.LinkedHashMap; 62 import java.util.Map; 63 import java.util.Set; 64 import java.util.jar.JarFile; 65 import java.util.jar.JarOutputStream; 66 import java.util.zip.ZipEntry; 67 import java.util.zip.ZipFile; 68 import java.util.zip.ZipOutputStream; 69 import org.apache.commons.io.FilenameUtils; 70 71 /** Driver for running {@link DefinitelyDerefedParams} */ 72 public class DefinitelyDerefedParamsDriver { 73 private static boolean DEBUG = false; 74 private static boolean VERBOSE = false; 75 LOG(boolean cond, String tag, String msg)76 private static void LOG(boolean cond, String tag, String msg) { 77 if (cond) { 78 System.out.println("[JI " + tag + "] " + msg); 79 } 80 } 81 82 String lastOutPath = ""; 83 private long analyzedBytes = 0; 84 private long analysisStartTime = 0; 85 private MethodParamAnnotations nonnullParams = new MethodParamAnnotations(); 86 private MethodReturnAnnotations nullableReturns = new MethodReturnAnnotations(); 87 88 private boolean annotateBytecode = false; 89 private boolean stripJarSignatures = false; 90 91 private static final String DEFAULT_ASTUBX_LOCATION = "META-INF/nullaway/jarinfer.astubx"; 92 private static final String ASTUBX_JAR_SUFFIX = ".astubx.jar"; 93 // TODO: Exclusions- 94 // org.ow2.asm : InvalidBytecodeException on 95 // com.ibm.wala.classLoader.ShrikeCTMethod.makeDecoder:110 96 private static final String DEFAULT_EXCLUSIONS = "org\\/objectweb\\/asm\\/.*"; 97 98 /** 99 * Accounts the bytecode size of analyzed method for statistics. 100 * 101 * @param mtd Analyzed method. 102 */ accountCodeBytes(IMethod mtd)103 private void accountCodeBytes(IMethod mtd) { 104 // Get method bytecode size 105 if (mtd instanceof ShrikeCTMethod) { 106 analyzedBytes += ((ShrikeCTMethod) mtd).getBytecodes().length; 107 } 108 } 109 getAnalysisDriver( IMethod mtd, AnalysisOptions options, AnalysisCache cache)110 private DefinitelyDerefedParams getAnalysisDriver( 111 IMethod mtd, AnalysisOptions options, AnalysisCache cache) { 112 IR ir = cache.getIRFactory().makeIR(mtd, Everywhere.EVERYWHERE, options.getSSAOptions()); 113 ControlFlowGraph<SSAInstruction, ISSABasicBlock> cfg = ir.getControlFlowGraph(); 114 accountCodeBytes(mtd); 115 return new DefinitelyDerefedParams(mtd, ir, cfg); 116 } 117 run(String inPaths, String pkgName, boolean includeNonPublicClasses)118 MethodParamAnnotations run(String inPaths, String pkgName, boolean includeNonPublicClasses) 119 throws IOException, ClassHierarchyException, IllegalArgumentException { 120 String outPath = ""; 121 String firstInPath = inPaths.split(",")[0]; 122 if (firstInPath.endsWith(".jar") || firstInPath.endsWith(".aar")) { 123 outPath = 124 FilenameUtils.getFullPath(firstInPath) 125 + FilenameUtils.getBaseName(firstInPath) 126 + ASTUBX_JAR_SUFFIX; 127 } else if (new File(firstInPath).exists()) { 128 outPath = FilenameUtils.getFullPath(firstInPath) + DEFAULT_ASTUBX_LOCATION; 129 } 130 return run(inPaths, pkgName, outPath, false, false, includeNonPublicClasses, DEBUG, VERBOSE); 131 } 132 run(String inPaths, String pkgName)133 MethodParamAnnotations run(String inPaths, String pkgName) 134 throws IOException, ClassHierarchyException, IllegalArgumentException { 135 return run(inPaths, pkgName, false); 136 } 137 runAndAnnotate( String inPaths, String pkgName, String outPath, boolean stripJarSignatures)138 MethodParamAnnotations runAndAnnotate( 139 String inPaths, String pkgName, String outPath, boolean stripJarSignatures) 140 throws IOException, ClassHierarchyException { 141 return run(inPaths, pkgName, outPath, true, stripJarSignatures, false, DEBUG, VERBOSE); 142 } 143 runAndAnnotate(String inPaths, String pkgName, String outPath)144 MethodParamAnnotations runAndAnnotate(String inPaths, String pkgName, String outPath) 145 throws IOException, ClassHierarchyException { 146 return runAndAnnotate(inPaths, pkgName, outPath, false); 147 } 148 149 /** 150 * Driver for the analysis. {@link DefinitelyDerefedParams} Usage: DefinitelyDerefedParamsDriver ( 151 * jar/aar_path, package_name, [output_path]) 152 * 153 * @param inPaths Comma separated paths to input jar/aar file to be analyzed. 154 * @param pkgName Qualified package name. 155 * @param outPath Path to output processed jar/aar file. Default outPath for 'a/b/c/x.jar' is 156 * 'a/b/c/x-ji.jar'. When 'annotatedBytecode' is enabled, this should refer to the directory 157 * that should contain all the output jars. 158 * @param annotateBytecode Perform bytecode transformation 159 * @param stripJarSignatures Remove jar cryptographic signatures 160 * @param includeNonPublicClasses Include non-public/ABI classes (e.g. for testing) 161 * @param dbg Output debug level logs 162 * @param vbs Output verbose level logs 163 * @return MethodParamAnnotations Map of 'method signatures' to their 'list of NonNull 164 * parameters'. 165 * @throws IOException on IO error. 166 * @throws ClassHierarchyException on Class Hierarchy factory error. 167 * @throws IllegalArgumentException on illegal argument to WALA API. 168 */ run( String inPaths, String pkgName, String outPath, boolean annotateBytecode, boolean stripJarSignatures, boolean includeNonPublicClasses, boolean dbg, boolean vbs)169 public MethodParamAnnotations run( 170 String inPaths, 171 String pkgName, 172 String outPath, 173 boolean annotateBytecode, 174 boolean stripJarSignatures, 175 boolean includeNonPublicClasses, 176 boolean dbg, 177 boolean vbs) 178 throws IOException, ClassHierarchyException { 179 DEBUG = dbg; 180 VERBOSE = vbs; 181 this.annotateBytecode = annotateBytecode; 182 this.stripJarSignatures = stripJarSignatures; 183 Set<String> setInPaths = new HashSet<>(Arrays.asList(inPaths.split(","))); 184 analysisStartTime = System.currentTimeMillis(); 185 for (String inPath : setInPaths) { 186 analyzeFile(pkgName, inPath, includeNonPublicClasses); 187 if (this.annotateBytecode) { 188 String outFile = outPath; 189 if (setInPaths.size() > 1) { 190 outFile = 191 outPath 192 + "/" 193 + FilenameUtils.getBaseName(inPath) 194 + "-annotated." 195 + FilenameUtils.getExtension(inPath); 196 } 197 writeAnnotations(inPath, outFile); 198 } 199 } 200 if (!this.annotateBytecode) { 201 new File(outPath).getParentFile().mkdirs(); 202 if (outPath.endsWith(".astubx")) { 203 writeModel(new DataOutputStream(new FileOutputStream(outPath))); 204 } else { 205 writeModelJAR(outPath); 206 } 207 } 208 lastOutPath = outPath; 209 return nonnullParams; 210 } 211 212 // Check if a method includes any dereferences at all at the bytecode level bytecodeHasAnyDereferences(IMethod mtd)213 private boolean bytecodeHasAnyDereferences(IMethod mtd) throws InvalidClassFileException { 214 // A dereference is either a field access (o.f) or a method call (o.m()) 215 return !CodeScanner.getFieldsRead(mtd).isEmpty() 216 || !CodeScanner.getFieldsWritten(mtd).isEmpty() 217 || !CodeScanner.getCallSites(mtd).isEmpty(); 218 } 219 analyzeFile(String pkgName, String inPath, boolean includeNonPublicClasses)220 private void analyzeFile(String pkgName, String inPath, boolean includeNonPublicClasses) 221 throws IOException, ClassHierarchyException { 222 InputStream jarIS = null; 223 if (inPath.endsWith(".jar") || inPath.endsWith(".aar")) { 224 jarIS = getInputStream(inPath); 225 if (jarIS == null) { 226 return; 227 } 228 } else if (!new File(inPath).exists()) { 229 return; 230 } 231 AnalysisScope scope = AnalysisScopeReader.instance.makeBasePrimordialScope(null); 232 scope.setExclusions( 233 new FileOfClasses( 234 new ByteArrayInputStream(DEFAULT_EXCLUSIONS.getBytes(StandardCharsets.UTF_8)))); 235 if (jarIS != null) { 236 scope.addInputStreamForJarToScope(ClassLoaderReference.Application, jarIS); 237 } else { 238 AnalysisScopeReader.instance.addClassPathToScope( 239 inPath, scope, ClassLoaderReference.Application); 240 } 241 AnalysisOptions options = new AnalysisOptions(scope, null); 242 AnalysisCache cache = new AnalysisCacheImpl(); 243 IClassHierarchy cha = ClassHierarchyFactory.makeWithRoot(scope); 244 Warnings.clear(); 245 246 // Iterate over all classes:methods in the 'Application' and 'Extension' class loaders 247 for (IClassLoader cldr : cha.getLoaders()) { 248 if (!cldr.getName().toString().equals("Primordial")) { 249 for (IClass cls : Iterator2Iterable.make(cldr.iterateAllClasses())) { 250 if (cls instanceof PhantomClass) { 251 continue; 252 } 253 // Only process classes in specified classpath and not its dependencies. 254 // TODO: figure the right way to do this 255 if (!pkgName.isEmpty() && !cls.getName().toString().startsWith(pkgName)) { 256 continue; 257 } 258 // Skip non-public / ABI classes 259 if (!cls.isPublic() && !includeNonPublicClasses) { 260 continue; 261 } 262 LOG(DEBUG, "DEBUG", "analyzing class: " + cls.getName().toString()); 263 for (IMethod mtd : Iterator2Iterable.make(cls.getDeclaredMethods().iterator())) { 264 // Skip methods without parameters, abstract methods, native methods 265 // some Application classes are Primordial (why?) 266 if (shouldCheckMethod(mtd)) { 267 Preconditions.checkNotNull(mtd, "method not found"); 268 DefinitelyDerefedParams analysisDriver = null; 269 String sign = ""; 270 try { 271 // Parameter analysis 272 if (mtd.getNumberOfParameters() > (mtd.isStatic() ? 0 : 1)) { 273 // For inferring parameter nullability, our criteria is based on finding 274 // unchecked dereferences of that parameter. We perform a quick bytecode 275 // check and skip methods containing no dereferences (i.e. method calls 276 // or field accesses) at all, avoiding the expensive IR/CFG generation 277 // step for these methods. 278 // Note that this doesn't apply to inferring return value nullability. 279 if (bytecodeHasAnyDereferences(mtd)) { 280 analysisDriver = getAnalysisDriver(mtd, options, cache); 281 Set<Integer> result = analysisDriver.analyze(); 282 sign = getSignature(mtd); 283 LOG(DEBUG, "DEBUG", "analyzed method: " + sign); 284 if (!result.isEmpty() || DEBUG) { 285 nonnullParams.put(sign, result); 286 LOG( 287 DEBUG, 288 "DEBUG", 289 "Inferred Nonnull param for method: " + sign + " = " + result.toString()); 290 } 291 } 292 } 293 // Return value analysis 294 analyzeReturnValue(options, cache, mtd, analysisDriver, sign); 295 } catch (Exception e) { 296 LOG( 297 DEBUG, 298 "DEBUG", 299 "Exception while scanning bytecodes for " + mtd + " " + e.getMessage()); 300 } 301 } 302 } 303 } 304 } 305 } 306 long endTime = System.currentTimeMillis(); 307 LOG( 308 VERBOSE, 309 "Stats", 310 inPath 311 + " >> time(ms): " 312 + (endTime - analysisStartTime) 313 + ", bytecode size: " 314 + analyzedBytes 315 + ", rate (ms/KB): " 316 + (analyzedBytes > 0 ? (((endTime - analysisStartTime) * 1000) / analyzedBytes) : 0)); 317 } 318 analyzeReturnValue( AnalysisOptions options, AnalysisCache cache, IMethod mtd, DefinitelyDerefedParams analysisDriver, String sign)319 private void analyzeReturnValue( 320 AnalysisOptions options, 321 AnalysisCache cache, 322 IMethod mtd, 323 DefinitelyDerefedParams analysisDriver, 324 String sign) { 325 if (!mtd.getReturnType().isPrimitiveType()) { 326 if (analysisDriver == null) { 327 analysisDriver = getAnalysisDriver(mtd, options, cache); 328 } 329 if (analysisDriver.analyzeReturnType() == DefinitelyDerefedParams.NullnessHint.NULLABLE) { 330 if (sign.isEmpty()) { 331 sign = getSignature(mtd); 332 } 333 nullableReturns.add(sign); 334 LOG(DEBUG, "DEBUG", "Inferred Nullable method return: " + sign); 335 } 336 } 337 } 338 shouldCheckMethod(IMethod mtd)339 private boolean shouldCheckMethod(IMethod mtd) { 340 return !mtd.isPrivate() 341 && !mtd.isAbstract() 342 && !mtd.isNative() 343 && !isAllPrimitiveTypes(mtd) 344 && !mtd.getDeclaringClass().getClassLoader().getName().toString().equals("Primordial"); 345 } 346 347 /** 348 * Checks if all parameters and return value of a method have primitive types. 349 * 350 * @param mtd Method. 351 * @return boolean True if all parameters and return value are of primitive type, otherwise false. 352 */ isAllPrimitiveTypes(IMethod mtd)353 private static boolean isAllPrimitiveTypes(IMethod mtd) { 354 if (!mtd.getReturnType().isPrimitiveType()) { 355 return false; 356 } 357 for (int i = (mtd.isStatic() ? 0 : 1); i < mtd.getNumberOfParameters(); i++) { 358 if (!mtd.getParameterType(i).isPrimitiveType()) { 359 return false; 360 } 361 } 362 return true; 363 } 364 365 /** 366 * Get InputStream of the jar of class files to be analyzed. 367 * 368 * @param libPath Path to input jar / aar file to be analyzed. 369 * @return InputStream InputStream for the jar. 370 */ getInputStream(String libPath)371 private static InputStream getInputStream(String libPath) throws IOException { 372 Preconditions.checkArgument( 373 (libPath.endsWith(".jar") || libPath.endsWith(".aar")) && Files.exists(Paths.get(libPath)), 374 "invalid library path! " + libPath); 375 LOG(VERBOSE, "Info", "opening library: " + libPath + "..."); 376 InputStream jarIS = null; 377 if (libPath.endsWith(".jar")) { 378 jarIS = new FileInputStream(libPath); 379 } else if (libPath.endsWith(".aar")) { 380 ZipFile aar = new ZipFile(libPath); 381 ZipEntry jarEntry = aar.getEntry("classes.jar"); 382 jarIS = (jarEntry == null ? null : aar.getInputStream(jarEntry)); 383 } 384 return jarIS; 385 } 386 387 /** 388 * Write model jar file with nullability model at DEFAULT_ASTUBX_LOCATION 389 * 390 * @param outPath Path of output model jar file. 391 */ writeModelJAR(String outPath)392 private void writeModelJAR(String outPath) throws IOException { 393 Preconditions.checkArgument( 394 outPath.endsWith(ASTUBX_JAR_SUFFIX), "invalid model file path! " + outPath); 395 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outPath)); 396 if (!nonnullParams.isEmpty()) { 397 ZipEntry entry = new ZipEntry(DEFAULT_ASTUBX_LOCATION); 398 // Set the modification/creation time to 0 to ensure that this jars always have the same 399 // checksum 400 entry.setTime(0); 401 entry.setCreationTime(FileTime.fromMillis(0)); 402 zos.putNextEntry(entry); 403 writeModel(new DataOutputStream(zos)); 404 zos.closeEntry(); 405 } 406 zos.close(); 407 LOG(VERBOSE, "Info", "wrote model to: " + outPath); 408 } 409 410 /** 411 * Write inferred nullability model in astubx format to the JarOutputStream for the processed 412 * jar/aar. 413 * 414 * @param out JarOutputStream for writing the astubx 415 */ 416 // Note: Need version compatibility check between generated stub files and when reading models 417 // StubxWriter.VERSION_0_FILE_MAGIC_NUMBER (?) writeModel(DataOutputStream out)418 private void writeModel(DataOutputStream out) throws IOException { 419 Map<String, String> importedAnnotations = 420 ImmutableMap.<String, String>builder() 421 .put("Nonnull", "javax.annotation.Nonnull") 422 .put("Nullable", "javax.annotation.Nullable") 423 .build(); 424 Map<String, Set<String>> packageAnnotations = new HashMap<>(); 425 Map<String, Set<String>> typeAnnotations = new HashMap<>(); 426 Map<String, MethodAnnotationsRecord> methodRecords = new LinkedHashMap<>(); 427 428 for (Map.Entry<String, Set<Integer>> entry : nonnullParams.entrySet()) { 429 String sign = entry.getKey(); 430 Set<Integer> ddParams = entry.getValue(); 431 if (ddParams.isEmpty()) { 432 continue; 433 } 434 Map<Integer, ImmutableSet<String>> argAnnotation = new HashMap<>(); 435 for (Integer param : ddParams) { 436 argAnnotation.put(param, ImmutableSet.of("Nonnull")); 437 } 438 methodRecords.put( 439 sign, 440 new MethodAnnotationsRecord( 441 nullableReturns.contains(sign) ? ImmutableSet.of("Nullable") : ImmutableSet.of(), 442 ImmutableMap.copyOf(argAnnotation))); 443 nullableReturns.remove(sign); 444 } 445 for (String nullableReturnMethodSign : Iterator2Iterable.make(nullableReturns.iterator())) { 446 methodRecords.put( 447 nullableReturnMethodSign, 448 new MethodAnnotationsRecord(ImmutableSet.of("Nullable"), ImmutableMap.of())); 449 } 450 StubxWriter.write(out, importedAnnotations, packageAnnotations, typeAnnotations, methodRecords); 451 } 452 writeAnnotations(String inPath, String outFile)453 private void writeAnnotations(String inPath, String outFile) throws IOException { 454 Preconditions.checkArgument( 455 inPath.endsWith(".jar") || inPath.endsWith(".aar") || inPath.endsWith(".class"), 456 "invalid input path - " + inPath); 457 LOG(DEBUG, "DEBUG", "Writing Annotations to " + outFile); 458 459 new File(outFile).getParentFile().mkdirs(); 460 if (inPath.endsWith(".jar")) { 461 JarFile jar = new JarFile(inPath); 462 JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(outFile)); 463 BytecodeAnnotator.annotateBytecodeInJar( 464 jar, jarOS, nonnullParams, nullableReturns, stripJarSignatures, DEBUG); 465 jarOS.close(); 466 } else if (inPath.endsWith(".aar")) { 467 ZipFile zip = new ZipFile(inPath); 468 ZipOutputStream zipOS = new ZipOutputStream(new FileOutputStream(outFile)); 469 BytecodeAnnotator.annotateBytecodeInAar( 470 zip, zipOS, nonnullParams, nullableReturns, stripJarSignatures, DEBUG); 471 zipOS.close(); 472 } else { 473 InputStream is = new FileInputStream(inPath); 474 OutputStream os = new FileOutputStream(outFile); 475 BytecodeAnnotator.annotateBytecodeInClass(is, os, nonnullParams, nullableReturns, DEBUG); 476 os.close(); 477 } 478 } 479 getSignature(IMethod mtd)480 private String getSignature(IMethod mtd) { 481 return annotateBytecode ? mtd.getSignature() : getAstubxSignature(mtd); 482 } 483 484 /** 485 * Get astubx style method signature. {FullyQualifiedEnclosingType}: {UnqualifiedMethodReturnType} 486 * {methodName} ([{UnqualifiedArgumentType}*]) 487 * 488 * @param mtd Method reference. 489 * @return String Method signature. 490 */ 491 // TODO: handle generics and inner classes getAstubxSignature(IMethod mtd)492 private static String getAstubxSignature(IMethod mtd) { 493 String classType = 494 mtd.getDeclaringClass().getName().toString().replaceAll("/", "\\.").substring(1); 495 classType = classType.replaceAll("\\$", "\\."); // handle inner class 496 String returnType = mtd.isInit() ? null : getSimpleTypeName(mtd.getReturnType()); 497 String strArgTypes = ""; 498 int argi = mtd.isStatic() ? 0 : 1; // Skip 'this' parameter 499 for (; argi < mtd.getNumberOfParameters(); argi++) { 500 strArgTypes += getSimpleTypeName(mtd.getParameterType(argi)); 501 if (argi < mtd.getNumberOfParameters() - 1) { 502 strArgTypes += ", "; 503 } 504 } 505 return classType 506 + ":" 507 + (returnType == null ? "void " : returnType + " ") 508 + mtd.getName().toString() 509 + "(" 510 + strArgTypes 511 + ")"; 512 } 513 514 /** 515 * Get simple unqualified type name. 516 * 517 * @param typ Type Reference. 518 * @return String Unqualified type name. 519 */ getSimpleTypeName(TypeReference typ)520 private static String getSimpleTypeName(TypeReference typ) { 521 final Map<String, String> mapFullTypeName = 522 ImmutableMap.<String, String>builder() 523 .put("B", "byte") 524 .put("C", "char") 525 .put("D", "double") 526 .put("F", "float") 527 .put("I", "int") 528 .put("J", "long") 529 .put("S", "short") 530 .put("Z", "boolean") 531 .build(); 532 if (typ.isArrayType()) { 533 return "Array"; 534 } 535 String typName = typ.getName().toString(); 536 if (typName.startsWith("L")) { 537 typName = typName.split("<")[0].substring(1); // handle generics 538 typName = typName.substring(typName.lastIndexOf('/') + 1); // get unqualified name 539 typName = typName.substring(typName.lastIndexOf('$') + 1); // handle inner classes 540 } else { 541 typName = mapFullTypeName.get(typName); 542 } 543 return typName; 544 } 545 } 546