Skip to content

Commit 6f4cda7

Browse files
authored
JSpecify: preserve explicit nullability annotations on type variables when performing substitutions (#1143)
Fixes #1091 Consider the following code: ```java abstract class Test { abstract <V> @nullable V foo(Function<@nullable V, @nullable V> f); void testPositive(Function<String, String> f) { this.<String>foo(f); // error } } ``` The call to `foo` should not type check. Since the type of its parameter `f` is `Function<@nullable V, @nullable V>`, with explicit `@Nullable` annotations on the type variables, any `Function` passed to `foo` must have `@Nullable` type arguments. In typechecking this code, NullAway previously substituted the type arguments for the type variables in `foo` just using built-in `javac` routines. But, this would yield a formal parameter type `Function<String, String>`, as the `javac` routine would not retain the explicit type arguments in the right places. So we would miss reporting an error. This PR fixes the substitutions and re-introduces the annotations on type variables, so we get the type `Function<@nullable String, @nullable String>` for the formal parameter at the call, and report an error correctly. Substitutions were broken in other cases as well; substituting `@Nullable V` for `@Nullable V` (where `V` is a type variable) yielded just `V`, which led to false positives (like #1091). The main logic changes are in `TypeSubstitutionUtils`. We add a new `RestoreNullnessAnnotationsVisitor` and use it to restore nullability annotations from type variables after performing a substitution. We also extract the `TypeMetadataBuilder` logic to a top-level source file, and add new methods as needed for this PR. Some of this could have been split into a separate PR but it's a bit of a pain to extract it now.
1 parent a1df1c4 commit 6f4cda7

File tree

7 files changed

+587
-196
lines changed

7 files changed

+587
-196
lines changed

nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private static NullnessStore lambdaInitialStore(
107107
// annotations in case of generic type arguments. Only used in JSpecify mode.
108108
List<Type> overridenMethodParamTypeList =
109109
TypeSubstitutionUtils.memberType(
110-
types, castToNonNull(ASTHelpers.getType(code)), fiMethodSymbol)
110+
types, castToNonNull(ASTHelpers.getType(code)), fiMethodSymbol, config)
111111
.getParameterTypes();
112112
// If fiArgumentPositionNullness[i] == null, parameter position i is unannotated
113113
Nullness[] fiArgumentPositionNullness = new Nullness[fiMethodParameters.size()];

nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -634,13 +634,14 @@ public static void compareGenericTypeParameterNullabilityForCall(
634634
}
635635
if (enclosingType != null) {
636636
invokedMethodType =
637-
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol);
637+
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config);
638638
}
639639
}
640640
// substitute type arguments for generic methods
641641
if (tree instanceof MethodInvocationTree && methodSymbol.type instanceof Type.ForAll) {
642642
invokedMethodType =
643-
substituteTypeArgsInGenericMethodType((MethodInvocationTree) tree, methodSymbol, state);
643+
substituteTypeArgsInGenericMethodType(
644+
(MethodInvocationTree) tree, methodSymbol, state, config);
644645
}
645646
List<Type> formalParamTypes = invokedMethodType.getParameterTypes();
646647
int n = formalParamTypes.size();
@@ -706,7 +707,7 @@ public static void checkTypeParameterNullnessForMethodOverriding(
706707
// method's class
707708
Type methodWithTypeParams =
708709
TypeSubstitutionUtils.memberType(
709-
state.getTypes(), overridingMethod.owner.type, overriddenMethod);
710+
state.getTypes(), overridingMethod.owner.type, overriddenMethod, analysis.getConfig());
710711

711712
checkTypeParameterNullnessForOverridingMethodReturnType(
712713
tree, methodWithTypeParams, analysis, state);
@@ -786,7 +787,7 @@ public static Nullness getGenericMethodReturnTypeNullness(
786787
return Nullness.NONNULL;
787788
}
788789
Type overriddenMethodType =
789-
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, method);
790+
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, method, config);
790791
verify(
791792
overriddenMethodType instanceof ExecutableType,
792793
"expected ExecutableType but instead got %s",
@@ -835,7 +836,8 @@ public static Nullness getGenericReturnNullnessAtInvocation(
835836
if (!invokedMethodSymbol.getTypeParameters().isEmpty()) {
836837
// Substitute type arguments inside the return type
837838
Type substitutedReturnType =
838-
substituteTypeArgsInGenericMethodType(tree, invokedMethodSymbol, state).getReturnType();
839+
substituteTypeArgsInGenericMethodType(tree, invokedMethodSymbol, state, config)
840+
.getReturnType();
839841
// If this condition evaluates to false, we fall through to the subsequent logic, to handle
840842
// type variables declared on the enclosing class
841843
if (substitutedReturnType != null
@@ -875,20 +877,22 @@ private static com.sun.tools.javac.util.List<Type> convertTreesToTypes(
875877
* @param methodInvocationTree the method invocation tree
876878
* @param methodSymbol symbol for the invoked generic method
877879
* @param state the visitor state
880+
* @param config the NullAway config
878881
* @return the substituted method type for the generic method
879882
*/
880883
private static Type substituteTypeArgsInGenericMethodType(
881884
MethodInvocationTree methodInvocationTree,
882885
Symbol.MethodSymbol methodSymbol,
883-
VisitorState state) {
886+
VisitorState state,
887+
Config config) {
884888

885889
List<? extends Tree> typeArgumentTrees = methodInvocationTree.getTypeArguments();
886890
com.sun.tools.javac.util.List<Type> explicitTypeArgs = convertTreesToTypes(typeArgumentTrees);
887891

888892
Type.ForAll forAllType = (Type.ForAll) methodSymbol.type;
889893
Type.MethodType underlyingMethodType = (Type.MethodType) forAllType.qtype;
890894
return TypeSubstitutionUtils.subst(
891-
state.getTypes(), underlyingMethodType, forAllType.tvars, explicitTypeArgs);
895+
state.getTypes(), underlyingMethodType, forAllType.tvars, explicitTypeArgs, config);
892896
}
893897

894898
/**
@@ -936,7 +940,7 @@ public static Nullness getGenericParameterNullnessAtInvocation(
936940
// Substitute the argument types within the MethodType
937941
// NOTE: if explicitTypeArgs is empty, this is a noop
938942
List<Type> substitutedParamTypes =
939-
substituteTypeArgsInGenericMethodType(tree, invokedMethodSymbol, state)
943+
substituteTypeArgsInGenericMethodType(tree, invokedMethodSymbol, state, config)
940944
.getParameterTypes();
941945
// If this condition evaluates to false, we fall through to the subsequent logic, to handle
942946
// type variables declared on the enclosing class
@@ -1022,7 +1026,8 @@ public static Nullness getGenericMethodParameterNullness(
10221026
// @Nullable annotation is handled elsewhere)
10231027
return Nullness.NONNULL;
10241028
}
1025-
Type methodType = TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, method);
1029+
Type methodType =
1030+
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, method, config);
10261031
Type paramType = methodType.getParameterTypes().get(parameterIndex);
10271032
return getTypeNullness(paramType, config);
10281033
}
Lines changed: 3 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.uber.nullaway.generics;
22

33
import static com.uber.nullaway.NullabilityUtil.castToNonNull;
4+
import static com.uber.nullaway.generics.TypeMetadataBuilder.TYPE_METADATA_BUILDER;
45

56
import com.google.errorprone.util.ASTHelpers;
67
import com.sun.source.tree.AnnotatedTypeTree;
@@ -14,12 +15,8 @@
1415
import com.sun.tools.javac.code.Symbol;
1516
import com.sun.tools.javac.code.Type;
1617
import com.sun.tools.javac.code.TypeMetadata;
17-
import com.sun.tools.javac.util.ListBuffer;
1818
import com.uber.nullaway.Config;
1919
import com.uber.nullaway.Nullness;
20-
import java.lang.invoke.MethodHandle;
21-
import java.lang.invoke.MethodHandles;
22-
import java.lang.invoke.MethodType;
2320
import java.util.ArrayList;
2421
import java.util.Collections;
2522
import java.util.List;
@@ -58,7 +55,8 @@ public Type visitParameterizedType(ParameterizedTypeTree tree, Void p) {
5855
for (int i = 0; i < typeArguments.size(); i++) {
5956
newTypeArgs.add(typeArguments.get(i).accept(this, null));
6057
}
61-
Type finalType = TYPE_METADATA_BUILDER.createWithBaseTypeAndTypeArgs(baseType, newTypeArgs);
58+
Type finalType =
59+
TYPE_METADATA_BUILDER.createClassType(baseType, baseType.getEnclosingType(), newTypeArgs);
6260
return finalType;
6361
}
6462

@@ -97,181 +95,4 @@ public Type visitAnnotatedType(AnnotatedTypeTree annotatedType, Void unused) {
9795
protected Type defaultAction(Tree node, Void unused) {
9896
return castToNonNull(ASTHelpers.getType(node));
9997
}
100-
101-
/**
102-
* Abstracts over the different APIs for building {@link TypeMetadata} objects in different JDK
103-
* versions.
104-
*/
105-
private interface TypeMetadataBuilder {
106-
TypeMetadata create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs);
107-
108-
Type cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metaData);
109-
110-
Type createWithBaseTypeAndTypeArgs(Type baseType, List<Type> typeArgs);
111-
}
112-
113-
/**
114-
* Provides implementations for methods under TypeMetadataBuilder compatible with JDK 17 and
115-
* earlier versions.
116-
*/
117-
private static class JDK17AndEarlierTypeMetadataBuilder implements TypeMetadataBuilder {
118-
119-
@Override
120-
public TypeMetadata create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs) {
121-
return new TypeMetadata(new TypeMetadata.Annotations(attrs));
122-
}
123-
124-
/**
125-
* Clones the given type with the specified Metadata for getting the right nullability
126-
* annotations.
127-
*
128-
* @param typeToBeCloned The Type we want to clone with the required Nullability Metadata
129-
* @param metadata The required Nullability metadata which is lost from the type
130-
* @return Type after it has been cloned by applying the required Nullability metadata
131-
*/
132-
@Override
133-
public Type cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metadata) {
134-
return typeToBeCloned.cloneWithMetadata(metadata);
135-
}
136-
137-
@Override
138-
public Type createWithBaseTypeAndTypeArgs(Type baseType, List<Type> typeArgs) {
139-
return new Type.ClassType(
140-
baseType.getEnclosingType(),
141-
com.sun.tools.javac.util.List.from(typeArgs),
142-
baseType.tsym,
143-
baseType.getMetadata());
144-
}
145-
}
146-
147-
/**
148-
* Provides implementations for methods under TypeMetadataBuilder compatible with the updates made
149-
* to the library methods for Jdk 21. The implementation calls the logic specific to JDK 21
150-
* indirectly using MethodHandles since we still need the code to compile on earlier versions.
151-
*/
152-
private static class JDK21TypeMetadataBuilder implements TypeMetadataBuilder {
153-
154-
private static final MethodHandle typeMetadataConstructorHandle = createHandle();
155-
private static final MethodHandle addMetadataHandle =
156-
createVirtualMethodHandle(Type.class, TypeMetadata.class, Type.class, "addMetadata");
157-
private static final MethodHandle dropMetadataHandle =
158-
createVirtualMethodHandle(Type.class, Class.class, Type.class, "dropMetadata");
159-
private static final MethodHandle getMetadataHandler = createGetMetadataHandle();
160-
private static final MethodHandle classTypeConstructorHandle =
161-
createClassTypeConstructorHandle();
162-
163-
private static MethodHandle createHandle() {
164-
MethodHandles.Lookup lookup = MethodHandles.lookup();
165-
MethodType mt = MethodType.methodType(void.class, com.sun.tools.javac.util.ListBuffer.class);
166-
try {
167-
return lookup.findConstructor(TypeMetadata.Annotations.class, mt);
168-
} catch (NoSuchMethodException e) {
169-
throw new RuntimeException(e);
170-
} catch (IllegalAccessException e) {
171-
throw new RuntimeException(e);
172-
}
173-
}
174-
175-
private static MethodHandle createGetMetadataHandle() {
176-
MethodHandles.Lookup lookup = MethodHandles.lookup();
177-
MethodType mt = MethodType.methodType(com.sun.tools.javac.util.List.class);
178-
try {
179-
return lookup.findVirtual(Type.class, "getMetadata", mt);
180-
} catch (NoSuchMethodException | IllegalAccessException e) {
181-
throw new RuntimeException(e);
182-
}
183-
}
184-
185-
private static MethodHandle createClassTypeConstructorHandle() {
186-
try {
187-
MethodHandles.Lookup lookup = MethodHandles.lookup();
188-
MethodType methodType =
189-
MethodType.methodType(
190-
void.class, // return type for a constructor is void
191-
Type.class,
192-
com.sun.tools.javac.util.List.class,
193-
Symbol.TypeSymbol.class,
194-
com.sun.tools.javac.util.List.class);
195-
return lookup.findConstructor(Type.ClassType.class, methodType);
196-
} catch (NoSuchMethodException | IllegalAccessException e) {
197-
throw new RuntimeException(e);
198-
}
199-
}
200-
201-
/**
202-
* Used to get a MethodHandle for a virtual method from the specified class
203-
*
204-
* @param retTypeClass Class to indicate the return type of the desired method
205-
* @param paramTypeClass Class to indicate the parameter type of the desired method
206-
* @param refClass Class within which the desired method is contained
207-
* @param methodName Name of the desired method
208-
* @return The appropriate MethodHandle for the virtual method
209-
*/
210-
private static MethodHandle createVirtualMethodHandle(
211-
Class<?> retTypeClass, Class<?> paramTypeClass, Class<?> refClass, String methodName) {
212-
MethodHandles.Lookup lookup = MethodHandles.lookup();
213-
MethodType mt = MethodType.methodType(retTypeClass, paramTypeClass);
214-
try {
215-
return lookup.findVirtual(refClass, methodName, mt);
216-
} catch (NoSuchMethodException e) {
217-
throw new RuntimeException(e);
218-
} catch (IllegalAccessException e) {
219-
throw new RuntimeException(e);
220-
}
221-
}
222-
223-
@Override
224-
public TypeMetadata create(com.sun.tools.javac.util.List<Attribute.TypeCompound> attrs) {
225-
ListBuffer<Attribute.TypeCompound> b = new ListBuffer<>();
226-
b.appendList(attrs);
227-
try {
228-
return (TypeMetadata) typeMetadataConstructorHandle.invoke(b);
229-
} catch (Throwable e) {
230-
throw new RuntimeException(e);
231-
}
232-
}
233-
234-
/**
235-
* Calls dropMetadata and addMetadata using MethodHandles for JDK 21, which removed the previous
236-
* cloneWithMetadata method.
237-
*
238-
* @param typeToBeCloned The Type we want to clone with the required Nullability metadata
239-
* @param metadata The required Nullability metadata
240-
* @return Cloned Type with the necessary Nullability metadata
241-
*/
242-
@Override
243-
public Type cloneTypeWithMetadata(Type typeToBeCloned, TypeMetadata metadata) {
244-
try {
245-
// In JDK 21 addMetadata works if there is no metadata associated with the type, so we
246-
// create a copy without the existing metadata first and then add it
247-
Type clonedTypeWithoutMetadata =
248-
(Type) dropMetadataHandle.invoke(typeToBeCloned, metadata.getClass());
249-
return (Type) addMetadataHandle.invoke(clonedTypeWithoutMetadata, metadata);
250-
} catch (Throwable e) {
251-
throw new RuntimeException(e);
252-
}
253-
}
254-
255-
@Override
256-
public Type createWithBaseTypeAndTypeArgs(Type baseType, List<Type> typeArgs) {
257-
try {
258-
com.sun.tools.javac.util.List<TypeMetadata> metadata =
259-
(com.sun.tools.javac.util.List<TypeMetadata>) getMetadataHandler.invoke(baseType);
260-
return (Type)
261-
classTypeConstructorHandle.invoke(
262-
baseType.getEnclosingType(),
263-
com.sun.tools.javac.util.List.from(typeArgs),
264-
baseType.tsym,
265-
metadata);
266-
} catch (Throwable e) {
267-
throw new RuntimeException(e);
268-
}
269-
}
270-
}
271-
272-
/** The TypeMetadataBuilder to be used for the current JDK version. */
273-
private static final TypeMetadataBuilder TYPE_METADATA_BUILDER =
274-
Runtime.version().feature() >= 21
275-
? new JDK21TypeMetadataBuilder()
276-
: new JDK17AndEarlierTypeMetadataBuilder();
27798
}

0 commit comments

Comments
 (0)