• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.uber.nullaway.generics;
2 
3 import static com.uber.nullaway.NullabilityUtil.castToNonNull;
4 
5 import com.google.common.base.Preconditions;
6 import com.google.errorprone.VisitorState;
7 import com.google.errorprone.util.ASTHelpers;
8 import com.sun.source.tree.AnnotatedTypeTree;
9 import com.sun.source.tree.AnnotationTree;
10 import com.sun.source.tree.ArrayTypeTree;
11 import com.sun.source.tree.ParameterizedTypeTree;
12 import com.sun.source.tree.Tree;
13 import com.sun.source.util.SimpleTreeVisitor;
14 import com.sun.tools.javac.code.Attribute;
15 import com.sun.tools.javac.code.Type;
16 import com.sun.tools.javac.code.TypeMetadata;
17 import com.sun.tools.javac.util.ListBuffer;
18 import java.lang.invoke.MethodHandle;
19 import java.lang.invoke.MethodHandles;
20 import java.lang.invoke.MethodType;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.List;
24 
25 /**
26  * Visitor For getting the preserved Annotation Types for the nested generic type arguments within a
27  * ParameterizedTypeTree. This is required primarily since javac does not preserve annotations on
28  * generic type arguments in its types for NewClassTrees. We need a visitor since the nested
29  * arguments may appear on different kinds of type trees, e.g., ArrayTypeTrees.
30  */
31 public class PreservedAnnotationTreeVisitor extends SimpleTreeVisitor<Type, Void> {
32 
33   private final VisitorState state;
34 
PreservedAnnotationTreeVisitor(VisitorState state)35   PreservedAnnotationTreeVisitor(VisitorState state) {
36     this.state = state;
37   }
38 
39   @Override
visitArrayType(ArrayTypeTree tree, Void p)40   public Type visitArrayType(ArrayTypeTree tree, Void p) {
41     Type elemType = tree.getType().accept(this, null);
42     return new Type.ArrayType(elemType, castToNonNull(ASTHelpers.getType(tree)).tsym);
43   }
44 
45   @Override
visitParameterizedType(ParameterizedTypeTree tree, Void p)46   public Type visitParameterizedType(ParameterizedTypeTree tree, Void p) {
47     Type.ClassType type = (Type.ClassType) ASTHelpers.getType(tree);
48     Preconditions.checkNotNull(type);
49     Type nullableType = GenericsChecks.JSPECIFY_NULLABLE_TYPE_SUPPLIER.get(state);
50     List<? extends Tree> typeArguments = tree.getTypeArguments();
51     List<Type> newTypeArgs = new ArrayList<>();
52     for (int i = 0; i < typeArguments.size(); i++) {
53       AnnotatedTypeTree annotatedType = null;
54       Tree curTypeArg = typeArguments.get(i);
55       // If the type argument has an annotation, it will either be an AnnotatedTypeTree, or a
56       // ParameterizedTypeTree in the case of a nested generic type
57       if (curTypeArg instanceof AnnotatedTypeTree) {
58         annotatedType = (AnnotatedTypeTree) curTypeArg;
59       } else if (curTypeArg instanceof ParameterizedTypeTree
60           && ((ParameterizedTypeTree) curTypeArg).getType() instanceof AnnotatedTypeTree) {
61         annotatedType = (AnnotatedTypeTree) ((ParameterizedTypeTree) curTypeArg).getType();
62       }
63       List<? extends AnnotationTree> annotations =
64           annotatedType != null ? annotatedType.getAnnotations() : Collections.emptyList();
65       boolean hasNullableAnnotation = false;
66       for (AnnotationTree annotation : annotations) {
67         if (ASTHelpers.isSameType(
68             nullableType, ASTHelpers.getType(annotation.getAnnotationType()), state)) {
69           hasNullableAnnotation = true;
70           break;
71         }
72       }
73       // construct a TypeMetadata object containing a nullability annotation if needed
74       com.sun.tools.javac.util.List<Attribute.TypeCompound> nullableAnnotationCompound =
75           hasNullableAnnotation
76               ? com.sun.tools.javac.util.List.from(
77                   Collections.singletonList(
78                       new Attribute.TypeCompound(
79                           nullableType, com.sun.tools.javac.util.List.nil(), null)))
80               : com.sun.tools.javac.util.List.nil();
81       TypeMetadata typeMetadata = TYPE_METADATA_BUILDER.create(nullableAnnotationCompound);
82       Type currentTypeArgType = curTypeArg.accept(this, null);
83       Type newTypeArgType =
84           TYPE_METADATA_BUILDER.cloneTypeWithMetadata(currentTypeArgType, typeMetadata);
85       newTypeArgs.add(newTypeArgType);
86     }
87     Type.ClassType finalType =
88         new Type.ClassType(
89             type.getEnclosingType(), com.sun.tools.javac.util.List.from(newTypeArgs), type.tsym);
90     return finalType;
91   }
92 
93   /** By default, just use the type computed by javac */
94   @Override
defaultAction(Tree node, Void unused)95   protected Type defaultAction(Tree node, Void unused) {
96     return castToNonNull(ASTHelpers.getType(node));
97   }
98 
99   /**
100    * Abstracts over the different APIs for building {@link TypeMetadata} objects in different JDK
101    * versions.
102    */
103   private interface TypeMetadataBuilder {
create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs)104     TypeMetadata create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs);
105 
cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metaData)106     Type cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metaData);
107 
createAnnotationsCtorHandle(Class<?> paramTypeClass)108     static MethodHandle createAnnotationsCtorHandle(Class<?> paramTypeClass) {
109       MethodHandles.Lookup lookup = MethodHandles.lookup();
110       MethodType mt = MethodType.methodType(void.class, paramTypeClass);
111       try {
112         return lookup.findConstructor(TypeMetadata.Annotations.class, mt);
113       } catch (NoSuchMethodException e) {
114         throw new RuntimeException(e);
115       } catch (IllegalAccessException e) {
116         throw new RuntimeException(e);
117       }
118     }
119 
120     /**
121      * Used to get a MethodHandle for a virtual method from the specified class
122      *
123      * @param retTypeClass Class to indicate the return type of the desired method
124      * @param paramTypeClass Class to indicate the parameter type of the desired method
125      * @param refClass Class within which the desired method is contained
126      * @param methodName Name of the desired method
127      * @return The appropriate MethodHandle for the virtual method
128      */
createVirtualMethodHandle( Class<?> retTypeClass, Class<?> paramTypeClass, Class<?> refClass, String methodName)129     static MethodHandle createVirtualMethodHandle(
130         Class<?> retTypeClass, Class<?> paramTypeClass, Class<?> refClass, String methodName) {
131       MethodHandles.Lookup lookup = MethodHandles.lookup();
132       MethodType mt = MethodType.methodType(retTypeClass, paramTypeClass);
133       try {
134         return lookup.findVirtual(refClass, methodName, mt);
135       } catch (NoSuchMethodException e) {
136         throw new RuntimeException(e);
137       } catch (IllegalAccessException e) {
138         throw new RuntimeException(e);
139       }
140     }
141 
142   }
143 
144   /**
145    * Provides implementations for methods under TypeMetadataBuilder compatible with JDK 17 and
146    * earlier versions.
147    */
148   private static class JDK17AndEarlierTypeMetadataBuilder implements TypeMetadataBuilder {
149 
150     private static final MethodHandle typeMetadataHandle =
151         TypeMetadataBuilder.createAnnotationsCtorHandle(com.sun.tools.javac.util.List.class);
152     private static final MethodHandle cloneHandle = TypeMetadataBuilder.createVirtualMethodHandle(
153             Type.class, TypeMetadata.class, Type.class, "cloneWithMetadata");
154 
155     @Override
create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs)156     public TypeMetadata create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs) {
157       try {
158         return (TypeMetadata) typeMetadataHandle.invoke(attrs);
159       } catch (Throwable e) {
160         throw new RuntimeException(e);
161       }
162     }
163 
164     /**
165      * Clones the given type with the specified Metadata for getting the right nullability
166      * annotations.
167      *
168      * @param typeToBeCloned The Type we want to clone with the required Nullability Metadata
169      * @param metadata The required Nullability metadata which is lost from the type
170      * @return Type after it has been cloned by applying the required Nullability metadata
171      */
172     @Override
cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metadata)173     public Type cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metadata) {
174       try {
175         return (Type) cloneHandle.invoke(typeToBeCloned, metadata);
176       } catch (Throwable e) {
177         throw new RuntimeException(e);
178       }
179     }
180   }
181 
182   /**
183    * Provides implementations for methods under TypeMetadataBuilder compatible with the updates made
184    * to the library methods for Jdk 21. The implementation calls the logic specific to JDK 21
185    * indirectly using MethodHandles since we still need the code to compile on earlier versions.
186    */
187   private static class JDK21TypeMetadataBuilder implements TypeMetadataBuilder {
188 
189     private static final MethodHandle typeMetadataHandle =
190         TypeMetadataBuilder.createAnnotationsCtorHandle(com.sun.tools.javac.util.ListBuffer.class);
191     private static final MethodHandle addMetadataHandle = TypeMetadataBuilder.createVirtualMethodHandle(
192             Type.class, TypeMetadata.class, Type.class, "addMetadata");
193     private static final MethodHandle dropMetadataHandle = TypeMetadataBuilder.createVirtualMethodHandle(
194             Type.class, Class.class, Type.class, "dropMetadata");
195 
196     @Override
create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs)197     public TypeMetadata create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs) {
198       ListBuffer<Attribute.TypeCompound> b = new ListBuffer<>();
199       b.appendList(attrs);
200       try {
201         return (TypeMetadata) typeMetadataHandle.invoke(b);
202       } catch (Throwable e) {
203         throw new RuntimeException(e);
204       }
205     }
206 
207     /**
208      * Calls dropMetadata and addMetadata using MethodHandles for JDK 21, which removed the previous
209      * cloneWithMetadata method.
210      *
211      * @param typeToBeCloned The Type we want to clone with the required Nullability metadata
212      * @param metadata The required Nullability metadata
213      * @return Cloned Type with the necessary Nullability metadata
214      */
215     @Override
cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metadata)216     public Type cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metadata) {
217       try {
218         // In JDK 21 addMetadata works if there is no metadata associated with the type, so we
219         // create a copy without the existing metadata first and then add it
220         Type clonedTypeWithoutMetadata =
221             (Type) dropMetadataHandle.invoke(typeToBeCloned, metadata.getClass());
222         return (Type) addMetadataHandle.invoke(clonedTypeWithoutMetadata, metadata);
223       } catch (Throwable e) {
224         throw new RuntimeException(e);
225       }
226     }
227   }
228 
229   /** The TypeMetadataBuilder to be used for the current JDK version. */
230   private static final TypeMetadataBuilder TYPE_METADATA_BUILDER =
231       getVersion() >= 21
232           ? new JDK21TypeMetadataBuilder()
233           : new JDK17AndEarlierTypeMetadataBuilder();
234 
235   /**
236    * Utility method to get the current JDK version, that works on Java 8 and above.
237    *
238    * <p>TODO remove this method once we drop support for Java 8
239    *
240    * @return the current JDK version
241    */
getVersion()242   private static int getVersion() {
243     String version = System.getProperty("java.version");
244     if (version.startsWith("1.")) {
245       version = version.substring(2, 3);
246     } else {
247       int dot = version.indexOf(".");
248       if (dot != -1) {
249         version = version.substring(0, dot);
250       }
251     }
252     return Integer.parseInt(version);
253   }
254 }
255