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