1 /* 2 * Copyright (C) 2019. 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 java.io.IOException; 20 import java.io.InputStream; 21 import java.util.Iterator; 22 import java.util.List; 23 import java.util.Map; 24 import java.util.jar.JarEntry; 25 import java.util.jar.JarFile; 26 import java.util.jar.JarInputStream; 27 import java.util.zip.ZipEntry; 28 import java.util.zip.ZipFile; 29 import org.objectweb.asm.ClassReader; 30 import org.objectweb.asm.Type; 31 import org.objectweb.asm.tree.AnnotationNode; 32 import org.objectweb.asm.tree.ClassNode; 33 import org.objectweb.asm.tree.MethodNode; 34 35 /** Class to check if the methods in the given class / jar files have the expected annotations. */ 36 public class AnnotationChecker { 37 private static final String expectNullableMethod = "expectNullable"; 38 private static final String expectNonnullParamsMethod = "expectNonnull"; 39 40 /** 41 * Checks if the given aar file contains the expected annotations. The annotations that are 42 * expected are specified in the form of a map. For example: map = {"ExpectNullable;", 43 * "Ljavax/annotation/Nullable;"} will check if all methods and params contain 44 * "Ljavax/annotation/Nullable;" iff "ExpectNullable;" is present. 45 * 46 * @param aarFile Path to the input aar file. 47 * @param expectedToActualAnnotations Map from 'Expect*' annotations to the actual annotations 48 * that are expected to be present. 49 * @return True when the actual annotations that are expected to be present are present iff the 50 * 'Expect*' annotations are present. 51 * @throws IOException if an error happens when reading the AAR file. 52 */ checkMethodAnnotationsInAar( String aarFile, Map<String, String> expectedToActualAnnotations)53 public static boolean checkMethodAnnotationsInAar( 54 String aarFile, Map<String, String> expectedToActualAnnotations) throws IOException { 55 Preconditions.checkArgument(aarFile.endsWith(".aar"), "invalid aar file: " + aarFile); 56 ZipFile zip = new ZipFile(aarFile); 57 Iterator<? extends ZipEntry> zipIterator = zip.stream().iterator(); 58 while (zipIterator.hasNext()) { 59 ZipEntry zipEntry = zipIterator.next(); 60 if (zipEntry.getName().equals("classes.jar")) { 61 JarInputStream jarIS = new JarInputStream(zip.getInputStream(zipEntry)); 62 JarEntry jarEntry = jarIS.getNextJarEntry(); 63 while (jarEntry != null) { 64 if (jarEntry.getName().endsWith(".class") 65 && !checkMethodAnnotationsInClass(jarIS, expectedToActualAnnotations)) { 66 return false; 67 } 68 jarEntry = jarIS.getNextJarEntry(); 69 } 70 } 71 } 72 return true; 73 } 74 75 /** 76 * Checks if the given jar file contains the expected annotations. The annotations that are 77 * expected are specified in the form of a map. For example: map = {"ExpectNullable;", 78 * "Ljavax/annotation/Nullable;"} will check if all methods and params contain 79 * "Ljavax/annotation/Nullable;" iff "ExpectNullable;" is present. 80 * 81 * @param jarFile Path to the input jar file. 82 * @param expectedToActualAnnotations Map from 'Expect*' annotations to the actual annotations 83 * that are expected to be present. 84 * @return True when the actual annotations that are expected to be present are present iff the 85 * 'Expect*' annotations are present. 86 * @throws IOException if an error happens when reading the jar file. 87 */ checkMethodAnnotationsInJar( String jarFile, Map<String, String> expectedToActualAnnotations)88 public static boolean checkMethodAnnotationsInJar( 89 String jarFile, Map<String, String> expectedToActualAnnotations) throws IOException { 90 Preconditions.checkArgument(jarFile.endsWith(".jar"), "invalid jar file: " + jarFile); 91 JarFile jar = new JarFile(jarFile); 92 for (JarEntry entry : (Iterable<JarEntry>) jar.stream()::iterator) { 93 if (entry.getName().endsWith(".class") 94 && !checkMethodAnnotationsInClass( 95 jar.getInputStream(entry), expectedToActualAnnotations)) { 96 return false; 97 } 98 } 99 return true; 100 } 101 checkMethodAnnotationsInClass( InputStream is, Map<String, String> expectedToActualAnnotations)102 private static boolean checkMethodAnnotationsInClass( 103 InputStream is, Map<String, String> expectedToActualAnnotations) throws IOException { 104 ClassReader cr = new ClassReader(is); 105 ClassNode cn = new ClassNode(); 106 cr.accept(cn, 0); 107 108 for (MethodNode method : cn.methods) { 109 if (!checkExpectedAnnotations(method.visibleAnnotations, expectedToActualAnnotations) 110 && !checkTestMethodAnnotationByName(method)) { 111 System.out.println( 112 "Error: Invalid / Unexpected annotations found on method '" + method.name + "'"); 113 return false; 114 } 115 List<AnnotationNode>[] paramAnnotations = method.visibleParameterAnnotations; 116 if (paramAnnotations == null) { 117 continue; 118 } 119 for (List<AnnotationNode> annotations : paramAnnotations) { 120 if (!checkExpectedAnnotations(annotations, expectedToActualAnnotations) 121 && !checkTestMethodParamAnnotationByName(method)) { 122 System.out.println( 123 "Error: Invalid / Unexpected annotations found in a parameter of method '" 124 + method.name 125 + "'."); 126 return false; 127 } 128 } 129 } 130 return true; 131 } 132 133 /** 134 * If the given method matches the expected test method name 'expectNullable', check if the method 135 * has the 'javax.annotation.Nullable' annotation on it exactly once. 136 * 137 * @param method method to be checked. 138 * @return True if 'javax.annotation.Nullable' is present exactly once on all matching methods. 139 */ checkTestMethodAnnotationByName(MethodNode method)140 private static boolean checkTestMethodAnnotationByName(MethodNode method) { 141 if (method.name.equals(expectNullableMethod)) { 142 return countAnnotations(method.visibleAnnotations, BytecodeAnnotator.javaxNullableDesc) == 1; 143 } 144 return true; 145 } 146 147 /** 148 * If the given method matches the expected test method name 'expectNonnull', check if all the 149 * parameters of the method has the 'javax.annotation.Nonnull' annotation on it exactly once. All 150 * such methods are also expected to have at least one parameter with this annotation. 151 * 152 * @param method method to be checked. 153 * @return True if 'javax.annotation.Nonnull' is present exactly once on all the parameters of 154 * matching methods. 155 */ checkTestMethodParamAnnotationByName(MethodNode method)156 private static boolean checkTestMethodParamAnnotationByName(MethodNode method) { 157 if (method.name.equals(expectNonnullParamsMethod)) { 158 int numParameters = Type.getArgumentTypes(method.desc).length; 159 if (numParameters == 0 160 || method.visibleParameterAnnotations == null 161 || method.visibleParameterAnnotations.length < numParameters) { 162 return false; 163 } 164 for (List<AnnotationNode> annotations : method.visibleParameterAnnotations) { 165 if (countAnnotations(annotations, BytecodeAnnotator.javaxNonnullDesc) != 1) { 166 return false; 167 } 168 } 169 } 170 return true; 171 } 172 checkExpectedAnnotations( List<AnnotationNode> annotations, Map<String, String> expectedToActualAnnotations)173 private static boolean checkExpectedAnnotations( 174 List<AnnotationNode> annotations, Map<String, String> expectedToActualAnnotations) { 175 for (Map.Entry<String, String> item : expectedToActualAnnotations.entrySet()) { 176 if (!checkExpectedAnnotation(annotations, item.getKey(), item.getValue())) { 177 return false; 178 } 179 } 180 return true; 181 } 182 183 // If `annotations` contain `expectAnnotation`: 184 // - Returns true iff `annotations` contain `actualAnnotation`, false otherwise. 185 // If `annotations` do not contain `expectAnnotation`: 186 // - Returns true iff `annotations` do not contain `actualAnnotation`, false otherwise. checkExpectedAnnotation( List<AnnotationNode> annotations, String expectAnnotation, String actualAnnotation)187 private static boolean checkExpectedAnnotation( 188 List<AnnotationNode> annotations, String expectAnnotation, String actualAnnotation) { 189 if (containsAnnotation(annotations, expectAnnotation)) { 190 int numAnnotationsFound = countAnnotations(annotations, actualAnnotation); 191 if (numAnnotationsFound != 1) { 192 System.out.println( 193 "Error: Annotation '" 194 + actualAnnotation 195 + "' was found " 196 + numAnnotationsFound 197 + " times."); 198 return false; 199 } 200 return true; 201 } 202 return !containsAnnotation(annotations, actualAnnotation); 203 } 204 205 // Returns true iff `annotation` is found in the list `annotations`, false otherwise. containsAnnotation(List<AnnotationNode> annotations, String annotation)206 private static boolean containsAnnotation(List<AnnotationNode> annotations, String annotation) { 207 return countAnnotations(annotations, annotation) > 0; 208 } 209 210 // Returns the number of times 'annotation' is present in the list 'annotations'. countAnnotations(List<AnnotationNode> annotations, String annotation)211 private static int countAnnotations(List<AnnotationNode> annotations, String annotation) { 212 if (annotations == null) { 213 return 0; 214 } 215 int count = 0; 216 for (AnnotationNode annotationNode : annotations) { 217 if (annotationNode.desc.equals(annotation)) { 218 count++; 219 } 220 } 221 return count; 222 } 223 } 224