diff --git a/nullaway/src/main/java/com/uber/nullaway/NullAway.java b/nullaway/src/main/java/com/uber/nullaway/NullAway.java index 00c36ba772..066d7ae087 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullAway.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullAway.java @@ -277,6 +277,8 @@ public Config getConfig() { */ private final Map computedNullnessMap = new LinkedHashMap<>(); + private GenericsChecks genericsChecks = new GenericsChecks(); + /** * Error Prone requires us to have an empty constructor for each Plugin, in addition to the * constructor taking an ErrorProneFlags object. This constructor should not be used anywhere @@ -500,7 +502,7 @@ public Description matchAssignment(AssignmentTree tree, VisitorState state) { } // generics check if (lhsType != null && config.isJSpecifyMode()) { - GenericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state); + genericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state); } if (config.isJSpecifyMode() && tree.getVariable() instanceof ArrayAccessTree) { @@ -1494,7 +1496,7 @@ public Description matchVariable(VariableTree tree, VisitorState state) { } VarSymbol symbol = ASTHelpers.getSymbol(tree); if (tree.getInitializer() != null && config.isJSpecifyMode()) { - GenericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state); + genericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state); } if (!config.isLegacyAnnotationLocation()) { checkNullableAnnotationPositionInType( @@ -1662,6 +1664,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { class2Entities.clear(); class2ConstructorUninit.clear(); computedNullnessMap.clear(); + genericsChecks.clearCache(); EnclosingEnvironmentNullness.instance(state.context).clear(); } else if (classAnnotationIntroducesPartialMarking(classSymbol)) { // Handle the case where the top-class is unannotated, but there is a @NullMarked annotation @@ -1880,13 +1883,13 @@ private Description handleInvocation( Nullness.paramHasNullableAnnotation(methodSymbol, i, config) ? Nullness.NULLABLE : ((config.isJSpecifyMode() && tree instanceof MethodInvocationTree) - ? GenericsChecks.getGenericParameterNullnessAtInvocation( + ? genericsChecks.getGenericParameterNullnessAtInvocation( i, methodSymbol, (MethodInvocationTree) tree, state, config) : Nullness.NONNULL); } } if (config.isJSpecifyMode()) { - GenericsChecks.compareGenericTypeParameterNullabilityForCall( + genericsChecks.compareGenericTypeParameterNullabilityForCall( methodSymbol, tree, actualParams, varArgsMethod, this, state); if (!methodSymbol.getTypeParameters().isEmpty()) { GenericsChecks.checkGenericMethodCallTypeArguments(tree, state, this, config, handler); @@ -2632,8 +2635,8 @@ private boolean mayBeNullMethodCall( return true; } if (config.isJSpecifyMode() - && GenericsChecks.getGenericReturnNullnessAtInvocation( - exprSymbol, invocationTree, state, config) + && genericsChecks + .getGenericReturnNullnessAtInvocation(exprSymbol, invocationTree, state, config) .equals(Nullness.NULLABLE)) { return true; } diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java index 88817b1a61..adaba1540a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java +++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java @@ -1086,8 +1086,9 @@ private boolean genericReturnIsNullable(MethodInvocationNode node) { MethodInvocationTree tree = node.getTree(); if (tree != null) { Nullness nullness = - GenericsChecks.getGenericReturnNullnessAtInvocation( - ASTHelpers.getSymbol(tree), tree, state, config); + new GenericsChecks() + .getGenericReturnNullnessAtInvocation( + ASTHelpers.getSymbol(tree), tree, state, config); return nullness.equals(NULLABLE); } } diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index fbe70b8ab1..e5223f8b0a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -26,6 +26,7 @@ import com.sun.tools.javac.code.TargetType; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.ListBuffer; import com.uber.nullaway.CodeAnnotationInfo; import com.uber.nullaway.Config; import com.uber.nullaway.ErrorBuilder; @@ -38,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeVariable; @@ -46,8 +48,12 @@ /** Methods for performing checks related to generic types and nullability. */ public final class GenericsChecks { - /** Do not instantiate; all methods should be static */ - private GenericsChecks() {} + /** + * Maps a MethodInvocationTree to a set of type variables that are mapped to their inferred types. + * Any generic type parameter that are not explicitly stated are inferred and cached in this + * field. + */ + private final Map> inferredTypes = new HashMap<>(); /** * Checks that for an instantiated generic type, {@code @Nullable} types are only used for type @@ -413,13 +419,16 @@ private static void reportInvalidOverridingMethodParamTypeError( * @param analysis the analysis object * @param state the visitor state */ - public static void checkTypeParameterNullnessForAssignability( + public void checkTypeParameterNullnessForAssignability( Tree tree, NullAway analysis, VisitorState state) { Config config = analysis.getConfig(); if (!config.isJSpecifyMode()) { return; } Type lhsType = getTreeType(tree, config); + if (lhsType == null) { + return; + } Tree rhsTree; if (tree instanceof VariableTree) { VariableTree varTree = (VariableTree) tree; @@ -435,7 +444,45 @@ public static void checkTypeParameterNullnessForAssignability( } Type rhsType = getTreeType(rhsTree, config); - if (lhsType != null && rhsType != null) { + if (rhsTree instanceof MethodInvocationTree) { + MethodInvocationTree methodInvocationTree = (MethodInvocationTree) rhsTree; + Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvocationTree); + // update inferredTypes cache for assignments + // generic method call with no explicit generic arguments + if (methodSymbol.type instanceof Type.ForAll + && methodInvocationTree.getTypeArguments().isEmpty()) { + InferTypeVisitor inferVisitor = new InferTypeVisitor(config); + // first infer through assignments + Type returnType = methodSymbol.getReturnType(); + returnType.accept(inferVisitor, lhsType); + + // then add any inference through parameter types + List argTypes = + methodInvocationTree.getArguments().stream() + .map(expr -> ASTHelpers.getType(expr)) + .collect(Collectors.toList()); + List paramTypes = methodSymbol.asType().getParameterTypes(); + ; + for (int i = 0; i < argTypes.size(); i++) { + if (i < paramTypes.size()) { + paramTypes.get(i).accept(inferVisitor, argTypes.get(i)); + } + } + + Map genericNullness = inferVisitor.getGenericNullnessMap(); + if (genericNullness != null) { + inferredTypes.put(methodInvocationTree, genericNullness); + if (rhsType != null) { + // recreate rhsType using inferredTypes + rhsType = + replaceTypeWithInference( + state, methodSymbol.getReturnType(), genericNullness, config); + } + } + } + } + + if (rhsType != null) { boolean isAssignmentValid = subtypeParameterNullability(lhsType, rhsType, state, config); if (!isAssignmentValid) { reportInvalidAssignmentInstantiationError(tree, lhsType, rhsType, state, analysis); @@ -443,6 +490,34 @@ public static void checkTypeParameterNullnessForAssignability( } } + /** + * Replaces any type variables in a type to their inferred types. + * + * @param state The visitor state + * @param typeToReplace The type with type variables to be replaced + * @param genericNullness The cache that maps type variables to its inferred types + * @param config Configuration for the analysis + * @return The replaced type + */ + private Type replaceTypeWithInference( + VisitorState state, + Type typeToReplace, + Map genericNullness, + Config config) { + ListBuffer typeVar = new ListBuffer<>(); + ListBuffer inference = new ListBuffer<>(); + for (Map.Entry entry : genericNullness.entrySet()) { + typeVar.append(entry.getKey()); + inference.append(entry.getValue()); + } + List keyTypeList = + typeVar.toList().stream().map(t -> (Type) t).collect(Collectors.toList()); + com.sun.tools.javac.util.List from = com.sun.tools.javac.util.List.from(keyTypeList); + com.sun.tools.javac.util.List to = inference.toList(); + + return TypeSubstitutionUtils.subst(state.getTypes(), typeToReplace, from, to, config); + } + /** * Checks that the nullability of type parameters for a returned expression matches that of the * type parameters of the enclosing method's return type. @@ -613,7 +688,7 @@ public static void checkTypeParameterNullnessForConditionalExpression( * @param analysis the analysis object * @param state the visitor state */ - public static void compareGenericTypeParameterNullabilityForCall( + public void compareGenericTypeParameterNullabilityForCall( Symbol.MethodSymbol methodSymbol, Tree tree, List actualParams, @@ -640,13 +715,36 @@ public static void compareGenericTypeParameterNullabilityForCall( TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config); } } - // substitute type arguments for generic methods + // substitute type arguments for generic methods with explicit type arguments if (tree instanceof MethodInvocationTree && methodSymbol.type instanceof Type.ForAll) { invokedMethodType = substituteTypeArgsInGenericMethodType( (MethodInvocationTree) tree, methodSymbol, state, config); } List formalParamTypes = invokedMethodType.getParameterTypes(); + // List newFormalParams = new ArrayList<>(formalParamTypes); + // // replace with inferred types + // if (tree instanceof MethodInvocationTree) { + // MethodInvocationTree methodInvocationTree = (MethodInvocationTree) tree; + // if (inferredTypes.containsKey(methodInvocationTree)) { + // Map genericNullness = inferredTypes.get(methodInvocationTree); + // // replace with inferred types + // for (int i = 0; i < formalParamTypes.size(); i++) { + // Type inferred = + // replaceTypeWithInference(state, formalParamTypes.get(i), genericNullness, + // config); + // newFormalParams.set(i, inferred); + // } + // } + // } + // // replace type variables that were not replaced + // for (int i = 0; i < newFormalParams.size(); i++) { + // Type currType = newFormalParams.get(i); + // if (currType instanceof Type.TypeVar) { + // Type upperBound = currType.getUpperBound(); + // newFormalParams.set(i, upperBound); + // } + // } int n = formalParamTypes.size(); if (isVarArgs) { // If the last argument is var args, don't check it now, it will be checked against @@ -830,7 +928,7 @@ public static Nullness getGenericMethodReturnTypeNullness( * @return Nullness of invocation's return type, or {@code NONNULL} if the call does not invoke an * instance method */ - public static Nullness getGenericReturnNullnessAtInvocation( + public Nullness getGenericReturnNullnessAtInvocation( Symbol.MethodSymbol invokedMethodSymbol, MethodInvocationTree tree, VisitorState state, @@ -883,7 +981,7 @@ private static com.sun.tools.javac.util.List convertTreesToTypes( * @param config the NullAway config * @return the substituted method type for the generic method */ - private static Type substituteTypeArgsInGenericMethodType( + private Type substituteTypeArgsInGenericMethodType( MethodInvocationTree methodInvocationTree, Symbol.MethodSymbol methodSymbol, VisitorState state, @@ -894,6 +992,15 @@ private static Type substituteTypeArgsInGenericMethodType( Type.ForAll forAllType = (Type.ForAll) methodSymbol.type; Type.MethodType underlyingMethodType = (Type.MethodType) forAllType.qtype; + + // if explicityTypeArgs are empty, there are no explicit type arguments + // so we need to get implicit types using the inferred types + if (explicitTypeArgs.isEmpty()) { + if (inferredTypes.containsKey(methodInvocationTree)) { + return replaceTypeWithInference( + state, underlyingMethodType, inferredTypes.get(methodInvocationTree), config); + } + } return TypeSubstitutionUtils.subst( state.getTypes(), underlyingMethodType, forAllType.tvars, explicitTypeArgs, config); } @@ -932,7 +1039,7 @@ private static Type substituteTypeArgsInGenericMethodType( * @return Nullness of parameter at {@code paramIndex}, or {@code NONNULL} if the call does not * invoke an instance method */ - public static Nullness getGenericParameterNullnessAtInvocation( + public Nullness getGenericParameterNullnessAtInvocation( int paramIndex, Symbol.MethodSymbol invokedMethodSymbol, MethodInvocationTree tree, @@ -952,6 +1059,21 @@ public static Nullness getGenericParameterNullnessAtInvocation( getTypeNullness(substitutedParamTypes.get(paramIndex), config), Nullness.NULLABLE)) { return Nullness.NULLABLE; } + // check nullness of inferred types + if (inferredTypes.containsKey(tree)) { + Map genericNullness = inferredTypes.get(tree); + List parameters = invokedMethodSymbol.getParameters(); + if (genericNullness.containsKey(parameters.get(paramIndex).type)) { + Type genericType = parameters.get(paramIndex).type; + Type inferredGenericType = genericNullness.get(genericType); + if (inferredGenericType != null + && Objects.equals(getTypeNullness(inferredGenericType, config), Nullness.NULLABLE)) { + return Nullness.NULLABLE; + } else { + return Nullness.NONNULL; + } + } + } } if (!(tree.getMethodSelect() instanceof MemberSelectTree) || invokedMethodSymbol.isStatic()) { @@ -1160,6 +1282,10 @@ public static boolean passingLambdaOrMethodRefWithGenericReturnToUnmarkedCode( return callingUnannotated; } + public void clearCache() { + inferredTypes.clear(); + } + public static boolean isNullableAnnotated(Type type, Config config) { return Nullness.hasNullableAnnotation(type.getAnnotationMirrors().stream(), config); } diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/InferTypeVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/InferTypeVisitor.java new file mode 100644 index 0000000000..063f56578a --- /dev/null +++ b/nullaway/src/main/java/com/uber/nullaway/generics/InferTypeVisitor.java @@ -0,0 +1,84 @@ +package com.uber.nullaway.generics; + +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Types; +import com.uber.nullaway.Config; +import com.uber.nullaway.Nullness; +import java.util.HashMap; +import java.util.Map; +import javax.lang.model.type.NullType; +import javax.lang.model.type.TypeVariable; + +/** Visitor that uses two types to infer the type of type variables. */ +public class InferTypeVisitor extends Types.DefaultTypeVisitor { + + private final Config config; + private final Map genericNullness = new HashMap<>(); + + InferTypeVisitor(Config config) { + this.config = config; + } + + @Override + public Void visitClassType(Type.ClassType rhsType, Type lhsType) { + if (rhsType instanceof NullType + || rhsType.isPrimitive() + || lhsType instanceof NullType + || lhsType.isPrimitive()) { + return null; + } + com.sun.tools.javac.util.List rhsTypeArguments = rhsType.getTypeArguments(); + com.sun.tools.javac.util.List lhsTypeArguments = + ((Type.ClassType) lhsType).getTypeArguments(); + // get the inferred type for each type arguments and add them to genericNullness + if (!rhsTypeArguments.isEmpty() && !lhsTypeArguments.isEmpty()) { + for (int i = 0; i < rhsTypeArguments.size(); i++) { + Type rhsTypeArg = rhsTypeArguments.get(i); + Type lhsTypeArg = lhsTypeArguments.get(i); + rhsTypeArg.accept(this, lhsTypeArg); + } + } + return null; + } + + @Override + public Void visitArrayType(Type.ArrayType rhsType, Type lhsType) { + if (!(lhsType instanceof Type.ArrayType)) { + return null; + } + // unwrap the type of the array and call accept on it + Type rhsComponentType = rhsType.elemtype; + Type lhsComponentType = ((Type.ArrayType) lhsType).elemtype; + rhsComponentType.accept(this, lhsComponentType); + return null; + } + + @Override + public Void visitTypeVar(Type.TypeVar rhsType, Type lhsType) { + if (genericNullness.containsKey(rhsType)) { + return null; // genericNullness already contains inference for this type + } + Boolean isLhsNullable = + Nullness.hasNullableAnnotation(lhsType.getAnnotationMirrors().stream(), config); + Type upperBound = rhsType.getUpperBound(); + Boolean isRhsNullable = + Nullness.hasNullableAnnotation(upperBound.getAnnotationMirrors().stream(), config); + if (!isLhsNullable) { // lhsType is NonNull, we can just use this + genericNullness.put(rhsType, lhsType); + } else if (isRhsNullable) { // lhsType & rhsType are Nullable, can use lhs for inference + genericNullness.put(rhsType, lhsType); + } else { // rhs can't be nullable, use upperbound + genericNullness.put(rhsType, upperBound); + } + return null; + } + + @Override + public Void visitType(Type t, Type type) { + return null; + } + + public Map getGenericNullnessMap() { + return this.genericNullness; + } +} diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java index 6f37df6b8e..d18588f55c 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java @@ -161,6 +161,255 @@ public void genericMethodAndVoidTypeWithInference() { .doTest(); } + @Test + public void genericInferenceOnAssignments() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + " class Test {", + " static class Foo {", + " Foo(T t) {}", + " static Foo makeNull(U u) {", + " return new Foo<>(u);", + " }", + " static Foo makeNonNull(U u) {", + " return new Foo<>(u);", + " }", + " }", + " static void testLocalAssign() {", + " // legal", + " Foo<@Nullable Object> f1 = Foo.makeNull(null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo f2 = Foo.makeNull(null);", + " Foo<@Nullable Object> f3 = Foo.makeNull(new Object());", + " Foo f4 = Foo.makeNull(new Object());", + " // ILLEGAL: U does not have a @Nullable upper bound", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo<@Nullable Object> f5 = Foo.makeNonNull(null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo f6 = Foo.makeNonNull(null);", + " // BUG: Diagnostic contains: due to mismatched nullability of type parameters", + " Foo<@Nullable Object> f7 = Foo.makeNonNull(new Object());", + " Foo f8 = Foo.makeNonNull(new Object());", + " }", + " }") + .doTest(); + } + + @Test + public void genericInferenceOnAssignmentAfterDeclaration() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + " class Test {", + " static class Foo {", + " Foo(T t) {}", + " static Foo makeNull(U u) {", + " return new Foo<>(u);", + " }", + " static Foo makeNonNull(U u) {", + " return new Foo<>(u);", + " }", + " }", + " static void testAssignAfterDeclaration() {", + " // legal", + " Foo<@Nullable Object> f1; f1 = Foo.makeNull(null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo f2; f2 = Foo.makeNull(null);", + " }", + " }") + .doTest(); + } + + @Test + public void multipleParametersOneNested() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " static class Foo {", + " Foo(T t) {}", + " static Foo create(U u, Foo other) {", + " return new Foo<>(u);", + " }", + " static void test(Foo<@Nullable Object> f1, Foo f2) {", + " // no error expected", + " Foo<@Nullable Object> result = Foo.create(null, f1);", + " // BUG: Diagnostic contains: has mismatched type parameter nullability", + " Foo<@Nullable Object> result2 = Foo.create(null, f2);", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void multipleParametersWithSomeInference() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " static class Foo {", + " Foo(T t) {}", + " static Foo create(U u, R r) {", + " return new Foo<>(u);", + " }", + " static void test() {", + " Foo<@Nullable Object> result = Foo.create(null, new String());", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void genericInferenceOnAssignmentsMultipleParams() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " class Foo {", + " Foo(T t) {}", + " public Foo make(U u, @Nullable String s) {", + " return new Foo<>(u);", + " }", + " }", + " static class Bar {", + " Bar(S s, Z z) {}", + " static Bar make(U u, B b) {", + " return new Bar<>(u, b);", + " }", + " }", + " static class Baz {", + " Baz(S s, Z z) {}", + " static Baz make(U u, B b) {", + " return new Baz<>(u, b);", + " }", + " }", + " public void run(Foo<@Nullable String> foo) {", + " // legal", + " Foo<@Nullable Object> f1 = foo.make(null, new String());", + " Foo<@Nullable Object> f2 = foo.make(null, null);", + " Foo f3 = foo.make(new Object(), null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo f4 = foo.make(null, null);", + " // legal", + " Bar<@Nullable Object, Object> b1 = Bar.make(null, new Object());", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Bar<@Nullable Object, Object> b2 = Bar.make(null, null);", + " Bar<@Nullable Object, @Nullable Object> b3 = Bar.make(null, null);", + " Bar b4 = Bar.make(new Object(), null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Bar b5 = Bar.make(null, null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Bar b6 = Bar.make(null, null);", + " // legal", + " Baz baz1 = Baz.make(new String(), new Object());", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Baz baz2 = Baz.make(null, new Object());", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Baz baz3 = Baz.make(new String(), null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Baz baz4 = Baz.make(null, null);", + " // BUG: Diagnostic contains: Generic type parameter cannot be @Nullable", + " Baz<@Nullable String, Object> baz5 = Baz.make(new String(), new Object());", + " // BUG: Diagnostic contains: Generic type parameter cannot be @Nullable", + " Baz baz6 = Baz.make(new String(), new Object());", + " }", + "}") + .doTest(); + } + + @Test + public void genericsUsedForGenericClasses() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "import java.util.ArrayList;", + "class Test {", + " abstract class Foo {", + " abstract Foo> nonNullTest();", + " abstract Foo> nullTest();", + " }", + " static void test(Foo f) {", + " Foo> fooNonNull_1 = f.nonNullTest();", + " // BUG: Diagnostic contains: due to mismatched nullability of type parameters", + " Foo> fooNonNull_2 = f.nonNullTest();", + " // BUG: Diagnostic contains: due to mismatched nullability of type parameters", + " Foo<@Nullable Integer, ArrayList> fooNonNull_3 = f.nonNullTest();", + " Foo> fooNull_1 = f.nullTest();", + " Foo> fooNull_2 = f.nullTest();", + " Foo<@Nullable Integer, ArrayList> fooNull_3 = f.nullTest();", + " }", + "}") + .doTest(); + } + + @Test + public void genericInferenceOnAssignmentsWithArrays() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + " class Test {", + " static class Foo {", + " Foo(T t) {}", + " static Foo[]> test1Null(U u) {", + " return new Foo<>((Foo[]) new Foo[5]);", + " }", + " static Foo[]> test1Nonnull(U u) {", + " return new Foo<>((Foo[]) new Foo[5]);", + " }", + " static Foo[] test2Null(U u) {", + " return (Foo[]) new Foo[5];", + " }", + " static Foo[] test2Nonnull(U u) {", + " return (Foo[]) new Foo[5];", + " }", + " }", + " static void testLocalAssign() {", + " Foo[]> f1 = Foo.test1Null(new Object());", + " Foo[]> f2 = Foo.test1Null(new Object());", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo[]> f3 = Foo.test1Null(null);", + " Foo[]> f4 = Foo.test1Null(null);", + " Foo[]> f5 = Foo.test1Nonnull(new Object());", + " // BUG: Diagnostic contains: due to mismatched nullability of type parameters", + " Foo[]> f6 = Foo.test1Nonnull(new Object());", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo[]> f7 = Foo.test1Nonnull(null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo[]> f8 = Foo.test1Nonnull(null);", + " Foo[] f9 = Foo.test2Null(new Object());", + " Foo<@Nullable Object>[] f10 = Foo.test2Null(new Object());", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo[] f11 = Foo.test2Null(null);", + " Foo<@Nullable Object>[] f12 = Foo.test2Null(null);", + " Foo[] f13 = Foo.test2Nonnull(new Object());", + " // BUG: Diagnostic contains: due to mismatched nullability of type parameters", + " Foo<@Nullable Object>[] f14 = Foo.test2Nonnull(new Object());", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo[] f15 = Foo.test2Nonnull(null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required", + " Foo<@Nullable Object>[] f16 = Foo.test2Nonnull(null);", + " }", + " }") + .doTest(); + } + @Test public void issue1035() { makeHelper()