• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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