diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java index 753f49431b..bb1395a6bf 100644 --- a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java +++ b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java @@ -53,6 +53,7 @@ public enum MessageTypes { WRONG_OVERRIDE_POSTCONDITION, WRONG_OVERRIDE_PRECONDITION, TYPE_PARAMETER_CANNOT_BE_NULLABLE, + ASSIGN_GENERIC_NULLABLE, } public String getMessage() { diff --git a/nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java index f7b0f3ac6e..4fdfa4b9d1 100644 --- a/nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java @@ -1,13 +1,25 @@ package com.uber.nullaway; +import static com.uber.nullaway.NullabilityUtil.castToNonNull; + +import com.google.common.base.Preconditions; import com.google.errorprone.VisitorState; +import com.google.errorprone.suppliers.Supplier; +import com.google.errorprone.suppliers.Suppliers; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.AnnotatedTypeTree; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.NewClassTree; import com.sun.source.tree.ParameterizedTypeTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.TypeMetadata; +import com.sun.tools.javac.code.Types; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,8 +27,18 @@ /** Methods for performing checks related to generic types and nullability. */ public final class GenericsChecks { - private GenericsChecks() { - // just utility methods + private static final String NULLABLE_NAME = "org.jspecify.annotations.Nullable"; + + private static final Supplier NULLABLE_TYPE_SUPPLIER = + Suppliers.typeFromString(NULLABLE_NAME); + private VisitorState state; + private Config config; + private NullAway analysis; + + public GenericsChecks(VisitorState state, Config config, NullAway analysis) { + this.state = state; + this.config = config; + this.analysis = analysis; } /** @@ -70,14 +92,14 @@ public static void checkInstantiationForParameterizedTypedTree( // if base type argument does not have @Nullable annotation then the instantiation is // invalid if (!hasNullableAnnotation) { - invalidInstantiationError( + reportInvalidInstantiationError( nullableTypeArguments.get(i), baseType, typeVariable, state, analysis); } } } } - private static void invalidInstantiationError( + private static void reportInvalidInstantiationError( Tree tree, Type baseType, Type baseTypeVariable, VisitorState state, NullAway analysis) { ErrorBuilder errorBuilder = analysis.getErrorBuilder(); ErrorMessage errorMessage = @@ -90,4 +112,194 @@ private static void invalidInstantiationError( errorBuilder.createErrorDescription( errorMessage, analysis.buildDescription(tree), state, null)); } + + private static void reportInvalidAssignmentInstantiationError( + Tree tree, Type lhsType, Type rhsType, VisitorState state, NullAway analysis) { + ErrorBuilder errorBuilder = analysis.getErrorBuilder(); + ErrorMessage errorMessage = + new ErrorMessage( + ErrorMessage.MessageTypes.ASSIGN_GENERIC_NULLABLE, + String.format( + "Cannot assign from type " + + rhsType + + " to type " + + lhsType + + " due to mismatched nullability of type parameters")); + state.reportMatch( + errorBuilder.createErrorDescription( + errorMessage, analysis.buildDescription(tree), state, null)); + } + + /** + * For a tree representing an assignment, ensures that from the perspective of type parameter + * nullability, the type of the right-hand side is assignable to (a subtype of) the type of the + * left-hand side. This check ensures that for every parameterized type nested in each of the + * types, the type parameters have identical nullability. + * + * @param tree the tree to check, which must be either an {@link AssignmentTree} or a {@link + * VariableTree} + */ + public void checkTypeParameterNullnessForAssignability(Tree tree) { + if (!config.isJSpecifyMode()) { + return; + } + Tree lhsTree; + Tree rhsTree; + if (tree instanceof VariableTree) { + VariableTree varTree = (VariableTree) tree; + lhsTree = varTree.getType(); + rhsTree = varTree.getInitializer(); + } else { + AssignmentTree assignmentTree = (AssignmentTree) tree; + lhsTree = assignmentTree.getVariable(); + rhsTree = assignmentTree.getExpression(); + } + // rhsTree can be null for a VariableTree. Also, we don't need to do a check + // if rhsTree is the null literal + if (rhsTree == null || rhsTree.getKind().equals(Tree.Kind.NULL_LITERAL)) { + return; + } + Type lhsType = ASTHelpers.getType(lhsTree); + Type rhsType = ASTHelpers.getType(rhsTree); + // For NewClassTrees with annotated type parameters, javac does not preserve the annotations in + // its computed type for the expression. As a workaround, we construct a replacement Type + // object with the appropriate annotations. + if (rhsTree instanceof NewClassTree + && ((NewClassTree) rhsTree).getIdentifier() instanceof ParameterizedTypeTree) { + ParameterizedTypeTree paramTypedTree = + (ParameterizedTypeTree) ((NewClassTree) rhsTree).getIdentifier(); + if (paramTypedTree.getTypeArguments().isEmpty()) { + // no explicit type parameters + return; + } + rhsType = typeWithPreservedAnnotations(paramTypedTree); + } + if (lhsType instanceof Type.ClassType && rhsType instanceof Type.ClassType) { + compareNullabilityAnnotations((Type.ClassType) lhsType, (Type.ClassType) rhsType, tree); + } + } + + /** + * Compare two types from an assignment for identical type parameter nullability, recursively + * checking nested generic types. See the JSpecify + * specification and the JLS + * subtyping rules for class and interface types. + * + * @param lhsType type for the lhs of the assignment + * @param rhsType type for the rhs of the assignment + * @param tree tree representing the assignment + */ + private void compareNullabilityAnnotations( + Type.ClassType lhsType, Type.ClassType rhsType, Tree tree) { + Types types = state.getTypes(); + // The base type of rhsType may be a subtype of lhsType's base type. In such cases, we must + // compare lhsType against the supertype of rhsType with a matching base type. + rhsType = (Type.ClassType) types.asSuper(rhsType, lhsType.tsym); + // This is impossible, considering the fact that standard Java subtyping succeeds before running + // NullAway + if (rhsType == null) { + throw new RuntimeException("Did not find supertype of " + rhsType + " matching " + lhsType); + } + List lhsTypeArguments = lhsType.getTypeArguments(); + List rhsTypeArguments = rhsType.getTypeArguments(); + // This is impossible, considering the fact that standard Java subtyping succeeds before running + // NullAway + if (lhsTypeArguments.size() != rhsTypeArguments.size()) { + throw new RuntimeException( + "Number of types arguments in " + rhsType + " does not match " + lhsType); + } + for (int i = 0; i < lhsTypeArguments.size(); i++) { + Type lhsTypeArgument = lhsTypeArguments.get(i); + Type rhsTypeArgument = rhsTypeArguments.get(i); + boolean isLHSNullableAnnotated = false; + List lhsAnnotations = lhsTypeArgument.getAnnotationMirrors(); + // To ensure that we are checking only jspecify nullable annotations + for (Attribute.TypeCompound annotation : lhsAnnotations) { + if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) { + isLHSNullableAnnotated = true; + break; + } + } + boolean isRHSNullableAnnotated = false; + List rhsAnnotations = rhsTypeArgument.getAnnotationMirrors(); + // To ensure that we are checking only jspecify nullable annotations + for (Attribute.TypeCompound annotation : rhsAnnotations) { + if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) { + isRHSNullableAnnotated = true; + break; + } + } + if (isLHSNullableAnnotated != isRHSNullableAnnotated) { + reportInvalidAssignmentInstantiationError(tree, lhsType, rhsType, state, analysis); + return; + } + // nested generics + if (lhsTypeArgument.getTypeArguments().length() > 0) { + compareNullabilityAnnotations( + (Type.ClassType) lhsTypeArgument, (Type.ClassType) rhsTypeArgument, tree); + } + } + } + + /** + * For the Parameterized typed trees, ASTHelpers.getType(tree) does not return a Type with + * preserved annotations. This method takes a Parameterized typed tree as an input and returns the + * Type of the tree with the annotations. + * + * @param tree A parameterized typed tree for which we need class type with preserved annotations. + * @return A Type with preserved annotations. + */ + private Type.ClassType typeWithPreservedAnnotations(ParameterizedTypeTree tree) { + Type.ClassType type = (Type.ClassType) ASTHelpers.getType(tree); + Preconditions.checkNotNull(type); + Type nullableType = NULLABLE_TYPE_SUPPLIER.get(state); + List typeArguments = tree.getTypeArguments(); + List newTypeArgs = new ArrayList<>(); + boolean hasNullableAnnotation = false; + for (int i = 0; i < typeArguments.size(); i++) { + AnnotatedTypeTree annotatedType = null; + Tree curTypeArg = typeArguments.get(i); + // If the type argument has an annotation, it will either be an AnnotatedTypeTree, or a + // ParameterizedTypeTree in the case of a nested generic type + if (curTypeArg instanceof AnnotatedTypeTree) { + annotatedType = (AnnotatedTypeTree) curTypeArg; + } else if (curTypeArg instanceof ParameterizedTypeTree + && ((ParameterizedTypeTree) curTypeArg).getType() instanceof AnnotatedTypeTree) { + annotatedType = (AnnotatedTypeTree) ((ParameterizedTypeTree) curTypeArg).getType(); + } + List annotations = + annotatedType != null ? annotatedType.getAnnotations() : Collections.emptyList(); + for (AnnotationTree annotation : annotations) { + if (ASTHelpers.isSameType( + nullableType, ASTHelpers.getType(annotation.getAnnotationType()), state)) { + hasNullableAnnotation = true; + break; + } + } + // construct a TypeMetadata object containing a nullability annotation if needed + com.sun.tools.javac.util.List nullableAnnotationCompound = + hasNullableAnnotation + ? com.sun.tools.javac.util.List.from( + Collections.singletonList( + new Attribute.TypeCompound( + nullableType, com.sun.tools.javac.util.List.nil(), null))) + : com.sun.tools.javac.util.List.nil(); + TypeMetadata typeMetadata = + new TypeMetadata(new TypeMetadata.Annotations(nullableAnnotationCompound)); + Type currentTypeArgType = castToNonNull(ASTHelpers.getType(curTypeArg)); + if (currentTypeArgType.getTypeArguments().size() > 0) { + // nested generic type; recursively preserve its nullability type argument annotations + currentTypeArgType = typeWithPreservedAnnotations((ParameterizedTypeTree) curTypeArg); + } + Type.ClassType newTypeArgType = + (Type.ClassType) currentTypeArgType.cloneWithMetadata(typeMetadata); + newTypeArgs.add(newTypeArgType); + } + Type.ClassType finalType = + new Type.ClassType( + type.getEnclosingType(), com.sun.tools.javac.util.List.from(newTypeArgs), type.tsym); + return finalType; + } } diff --git a/nullaway/src/main/java/com/uber/nullaway/NullAway.java b/nullaway/src/main/java/com/uber/nullaway/NullAway.java index ce5ad948b9..7fe7fdbdb1 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullAway.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullAway.java @@ -466,6 +466,11 @@ public Description matchAssignment(AssignmentTree tree, VisitorState state) { if (lhsType != null && lhsType.isPrimitive()) { doUnboxingCheck(state, tree.getExpression()); } + // generics check + if (lhsType != null && lhsType.getTypeArguments().length() > 0) { + new GenericsChecks(state, config, this).checkTypeParameterNullnessForAssignability(tree); + } + Symbol assigned = ASTHelpers.getSymbol(tree.getVariable()); if (assigned == null || assigned.getKind() != ElementKind.FIELD) { // not a field of nullable type @@ -1333,6 +1338,10 @@ public Description matchVariable(VariableTree tree, VisitorState state) { return Description.NO_MATCH; } VarSymbol symbol = ASTHelpers.getSymbol(tree); + if (tree.getInitializer() != null) { + new GenericsChecks(state, config, this).checkTypeParameterNullnessForAssignability(tree); + } + if (symbol.type.isPrimitive() && tree.getInitializer() != null) { doUnboxingCheck(state, tree.getInitializer()); } diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java index 3a0396792d..18f2e69b84 100644 --- a/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java @@ -14,14 +14,14 @@ public void basicTypeParamInstantiation() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam {}", - " static class NullableTypeParam {}", + " static class NonNullTypeParam {}", + " static class NullableTypeParam {}", + " // BUG: Diagnostic contains: Generic type parameter", + " static void testBadNonNull(NonNullTypeParam<@Nullable String> t1) {", " // BUG: Diagnostic contains: Generic type parameter", - " static void testBadNonNull(NonNullTypeParam<@Nullable String> t1) {", - " // BUG: Diagnostic contains: Generic type parameter", - " NonNullTypeParam<@Nullable String> t2 = null;", - " NullableTypeParam<@Nullable String> t3 = null;", - " }", + " NonNullTypeParam<@Nullable String> t2 = null;", + " NullableTypeParam<@Nullable String> t3 = null;", + " }", "}") .doTest(); } @@ -34,24 +34,25 @@ public void constructorTypeParamInstantiation() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam {}", - " static class NullableTypeParam {}", - " static void testOkNonNull(NonNullTypeParam t) {", - " NonNullTypeParam t2 = new NonNullTypeParam();", - " }", - " static void testBadNonNull(NonNullTypeParam t) {", - " // BUG: Diagnostic contains: Generic type parameter", - " NonNullTypeParam t2 = new NonNullTypeParam<@Nullable String>();", - " // BUG: Diagnostic contains: Generic type parameter", - " testBadNonNull(new NonNullTypeParam<@Nullable String>());", - " testBadNonNull(new NonNullTypeParam<", - " // BUG: Diagnostic contains: Generic type parameter", - " @Nullable String>());", - " }", - " static void testOkNullable(NullableTypeParam t1, NullableTypeParam<@Nullable String> t2) {", - " NullableTypeParam t3 = new NullableTypeParam();", - " NullableTypeParam<@Nullable String> t4 = new NullableTypeParam<@Nullable String>();", - " }", + " static class NonNullTypeParam {}", + " static class NullableTypeParam {}", + " static void testOkNonNull(NonNullTypeParam t) {", + " NonNullTypeParam t2 = new NonNullTypeParam();", + " }", + " static void testBadNonNull(NonNullTypeParam t) {", + " // BUG: Diagnostic contains: Generic type parameter", + " NonNullTypeParam t2 = new NonNullTypeParam<@Nullable String>();", + " // BUG: Diagnostic contains: Generic type parameter", + " testBadNonNull(new NonNullTypeParam<@Nullable String>());", + " testBadNonNull(", + " new NonNullTypeParam<", + " // BUG: Diagnostic contains: Generic type parameter", + " @Nullable String>());", + " }", + " static void testOkNullable(NullableTypeParam t1, NullableTypeParam<@Nullable String> t2) {", + " NullableTypeParam t3 = new NullableTypeParam();", + " NullableTypeParam<@Nullable String> t4 = new NullableTypeParam<@Nullable String>();", + " }", "}") .doTest(); } @@ -64,14 +65,20 @@ public void multipleTypeParametersInstantiation() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class MixedTypeParam {}", - " // BUG: Diagnostic contains: Generic type parameter", - " static class PartiallyInvalidSubclass extends MixedTypeParam<@Nullable String, String, String, @Nullable String> {}", - " static class ValidSubclass1 extends MixedTypeParam {}", - " static class PartiallyInvalidSubclass2 extends MixedTypeParam {}", - " static class ValidSubclass2 extends MixedTypeParam {}", + " static class MixedTypeParam {}", + " static class PartiallyInvalidSubclass", + " // BUG: Diagnostic contains: Generic type parameter", + " extends MixedTypeParam<@Nullable String, String, String, @Nullable String> {}", + " static class ValidSubclass1", + " extends MixedTypeParam {}", + " static class PartiallyInvalidSubclass2", + " extends MixedTypeParam<", + " String,", + " String,", + " String,", + " // BUG: Diagnostic contains: Generic type parameter", + " @Nullable String> {}", + " static class ValidSubclass2 extends MixedTypeParam {}", "}") .doTest(); } @@ -84,13 +91,13 @@ public void subClassTypeParamInstantiation() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam {}", - " static class NullableTypeParam {}", - " static class SuperClassForValidSubclass {", - " static class ValidSubclass extends NullableTypeParam<@Nullable String> {}", - " // BUG: Diagnostic contains: Generic type parameter", - " static class InvalidSubclass extends NonNullTypeParam<@Nullable String> {}", - " }", + " static class NonNullTypeParam {}", + " static class NullableTypeParam {}", + " static class SuperClassForValidSubclass {", + " static class ValidSubclass extends NullableTypeParam<@Nullable String> {}", + " // BUG: Diagnostic contains: Generic type parameter", + " static class InvalidSubclass extends NonNullTypeParam<@Nullable String> {}", + " }", "}") .doTest(); } @@ -103,11 +110,12 @@ public void interfaceImplementationTypeParamInstantiation() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static interface NonNullTypeParamInterface{}", - " static interface NullableTypeParamInterface{}", - " // BUG: Diagnostic contains: Generic type parameter", - " static class InvalidInterfaceImplementation implements NonNullTypeParamInterface<@Nullable String> {}", - " static class ValidInterfaceImplementation implements NullableTypeParamInterface {}", + " static interface NonNullTypeParamInterface {}", + " static interface NullableTypeParamInterface {}", + " static class InvalidInterfaceImplementation", + " // BUG: Diagnostic contains: Generic type parameter", + " implements NonNullTypeParamInterface<@Nullable String> {}", + " static class ValidInterfaceImplementation implements NullableTypeParamInterface {}", "}") .doTest(); } @@ -120,17 +128,17 @@ public void nestedTypeParams() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam {}", - " static class NullableTypeParam {}", + " static class NonNullTypeParam {}", + " static class NullableTypeParam {}", + " // BUG: Diagnostic contains: Generic type parameter", + " static void testBadNonNull(NullableTypeParam> t) {", " // BUG: Diagnostic contains: Generic type parameter", - " static void testBadNonNull(NullableTypeParam> t) {", - " // BUG: Diagnostic contains: Generic type parameter", - " NullableTypeParam>> t2 = null;", - " // BUG: Diagnostic contains: Generic type parameter", - " t2 = new NullableTypeParam>>();", - " // this is fine", - " NullableTypeParam>> t3 = null;", - " }", + " NullableTypeParam>> t2 = null;", + " // BUG: Diagnostic contains: Generic type parameter", + " t2 = new NullableTypeParam>>();", + " // this is fine", + " NullableTypeParam>> t3 = null;", + " }", "}") .doTest(); } @@ -143,15 +151,15 @@ public void returnTypeParamInstantiation() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam {}", - " static class NullableTypeParam {}", - " // BUG: Diagnostic contains: Generic type parameter", - " static NonNullTypeParam<@Nullable String> testBadNonNull() {", - " return new NonNullTypeParam();", - " }", - " static NullableTypeParam<@Nullable String> testOKNull() {", - " return new NullableTypeParam<@Nullable String>();", - " }", + " static class NonNullTypeParam {}", + " static class NullableTypeParam {}", + " // BUG: Diagnostic contains: Generic type parameter", + " static NonNullTypeParam<@Nullable String> testBadNonNull() {", + " return new NonNullTypeParam();", + " }", + " static NullableTypeParam<@Nullable String> testOKNull() {", + " return new NullableTypeParam<@Nullable String>();", + " }", "}") .doTest(); } @@ -165,18 +173,18 @@ public void testOKNewClassInstantiationForOtherAnnotations() { "import lombok.NonNull;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam {}", - " static class DifferentAnnotTypeParam1 {}", - " static class DifferentAnnotTypeParam2<@NonNull E> {}", - " static void testOKOtherAnnotation(NonNullTypeParam t) {", - " // should not show error for annotation other than @Nullable", - " testOKOtherAnnotation(new NonNullTypeParam<@NonNull String>());", - " DifferentAnnotTypeParam1 t1 = new DifferentAnnotTypeParam1();", - " // BUG: Diagnostic contains: Generic type parameter", - " DifferentAnnotTypeParam2 t2 = new DifferentAnnotTypeParam2<@Nullable String>();", - " // BUG: Diagnostic contains: Generic type parameter", - " DifferentAnnotTypeParam1 t3 = new DifferentAnnotTypeParam1<@Nullable String>();", - " }", + " static class NonNullTypeParam {}", + " static class DifferentAnnotTypeParam1 {}", + " static class DifferentAnnotTypeParam2<@NonNull E> {}", + " static void testOKOtherAnnotation(NonNullTypeParam t) {", + " // should not show error for annotation other than @Nullable", + " testOKOtherAnnotation(new NonNullTypeParam<@NonNull String>());", + " DifferentAnnotTypeParam1 t1 = new DifferentAnnotTypeParam1();", + " // BUG: Diagnostic contains: Generic type parameter", + " DifferentAnnotTypeParam2 t2 = new DifferentAnnotTypeParam2<@Nullable String>();", + " // BUG: Diagnostic contains: Generic type parameter", + " DifferentAnnotTypeParam1 t3 = new DifferentAnnotTypeParam1<@Nullable String>();", + " }", "}") .doTest(); } @@ -189,7 +197,7 @@ public void downcastInstantiation() { "package com.uber;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam { }", + " static class NonNullTypeParam {}", " static void instOf(Object o) {", " // BUG: Diagnostic contains: Generic type parameter", " Object p = (NonNullTypeParam<@Nullable String>) o;", @@ -207,7 +215,7 @@ public void instantiationInUnannotatedCode() { "package com.other;", "import org.jspecify.annotations.Nullable;", "class Test {", - " static class NonNullTypeParam { }", + " static class NonNullTypeParam {}", " static void instOf(Object o) {", " Object p = (NonNullTypeParam<@Nullable String>) o;", " }", @@ -215,6 +223,271 @@ public void instantiationInUnannotatedCode() { .doTest(); } + @Test + public void genericsChecksForAssignments() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " static class NullableTypeParam {}", + " static void testPositive(NullableTypeParam<@Nullable String> t1) {", + " // BUG: Diagnostic contains: Cannot assign from type", + " NullableTypeParam t2 = t1;", + " }", + " static void testNegative(NullableTypeParam<@Nullable String> t1) {", + " NullableTypeParam<@Nullable String> t2 = t1;", + " }", + "}") + .doTest(); + } + + @Test + public void nestedChecksForAssignmentsMultipleArguments() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " static class SampleClass {}", + " static class SampleClassMultipleArguments {}", + " static void testPositive() {", + " // BUG: Diagnostic contains: Cannot assign from type", + " SampleClassMultipleArguments>, String> t1 =", + " new SampleClassMultipleArguments>, String>();", + " }", + " static void testNegative() {", + " SampleClassMultipleArguments>, String> t1 =", + " new SampleClassMultipleArguments>, String>();", + " }", + "}") + .doTest(); + } + + @Test + public void superTypeAssignmentChecksSingleInterface() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " interface Fn

{}", + " class FnImpl implements Fn<@Nullable String, @Nullable String> {}", + " void testPositive() {", + " // BUG: Diagnostic contains: Cannot assign from type", + " Fn<@Nullable String, String> f = new FnImpl();", + " }", + " void testNegative() {", + " Fn<@Nullable String, @Nullable String> f = new FnImpl();", + " }", + "}") + .doTest(); + } + + @Test + public void superTypeAssignmentChecksMultipleInterface() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " interface Fn1 {}", + " interface Fn2

{}", + " class FnImpl implements Fn1<@Nullable String, @Nullable String>, Fn2 {}", + " void testPositive() {", + " // BUG: Diagnostic contains: Cannot assign from type", + " Fn2<@Nullable String> f = new FnImpl();", + " }", + " void testNegative() {", + " Fn2 f = new FnImpl();", + " }", + "}") + .doTest(); + } + + @Test + public void superTypeAssignmentChecksMultipleLevelInheritance() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " class SuperClassC {}", + " class SuperClassB

extends SuperClassC

{}", + " class SubClassA

extends SuperClassB

{}", + " class FnImpl1 extends SubClassA {}", + " class FnImpl2 extends SubClassA<@Nullable String> {}", + " void testPositive() {", + " SuperClassC<@Nullable String> f;", + " // BUG: Diagnostic contains: Cannot assign from type", + " f = new FnImpl1();", + " }", + " void testNegative() {", + " SuperClassC<@Nullable String> f;", + " // No error", + " f = new FnImpl2();", + " }", + "}") + .doTest(); + } + + @Test + public void subtypeWithParameters() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " class D

{}", + " class B

extends D

{}", + " void testPositive(B<@Nullable String> b) {", + " // BUG: Diagnostic contains: Cannot assign from type", + " D f1 = new B<@Nullable String>();", + " // BUG: Diagnostic contains: Cannot assign from type", + " D f2 = b;", + " }", + " void testNegative(B b) {", + " D f1 = new B();", + " D f2 = b;", + " }", + "}") + .doTest(); + } + + @Test + public void fancierSubtypeWithParameters() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " class Super {}", + " class Sub extends Super {}", + " void testNegative() {", + " // valid assignment", + " Super<@Nullable String, String> s = new Sub();", + " }", + " void testPositive() {", + " // BUG: Diagnostic contains: Cannot assign from type", + " Super<@Nullable String, String> s2 = new Sub<@Nullable String, String>();", + " }", + "}") + .doTest(); + } + + @Test + public void nestedVariableDeclarationChecks() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " class D

{}", + " class B

extends D

{}", + " class C

{}", + " class A, P extends @Nullable Object> {}", + " void testPositive() {", + " // BUG: Diagnostic contains: Cannot assign from type", + " D> f1 = new B>();", + " // BUG: Diagnostic contains: Cannot assign from type", + " A, String> f2 = new A, @Nullable String>();", + " // BUG: Diagnostic contains: Cannot assign from type", + " D> f3 = new B<@Nullable C>();", + " }", + " void testNegative() {", + " D> f1 = new B>();", + " A, String> f2 = new A, String>();", + " D<@Nullable C> f3 = new B<@Nullable C>();", + " }", + "}") + .doTest(); + } + + @Test + public void testForMethodReferenceInAnAssignment() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " interface A {", + " String function(T1 o);", + " }", + " static String foo(Object o) {", + " return o.toString();", + " }", + " static void testPositive() {", + " // TODO: we should report an error here, since Test::foo cannot take", + " // a @Nullable parameter. we don't catch this yet", + " A<@Nullable Object> p = Test::foo;", + " }", + " static void testNegative() {", + " A p = Test::foo;", + " }", + "}") + .doTest(); + } + + @Test + public void testForLambdasInAnAssignment() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " interface A {", + " String function(T1 o);", + " }", + " static void testPositive() {", + " // TODO: we should report an error here, since the lambda cannot take", + " // a @Nullable parameter. we don't catch this yet", + " A<@Nullable Object> p = o -> o.toString();", + " }", + " static void testNegative() {", + " A p = o -> o.toString();", + " }", + "}") + .doTest(); + } + + @Test + public void testForDiamondInAnAssignment() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "class Test {", + " interface A {", + " String function(T1 o);", + " }", + " static class B implements A {", + " public String function(T1 o) {", + " return o.toString();", + " }", + " }", + " static void testPositive() {", + " // TODO: we should report an error here, since B's type parameter", + " // cannot be @Nullable; we do not catch this yet", + " A<@Nullable Object> p = new B<>();", + " }", + " static void testNegative() {", + " A p = new B<>();", + " }", + "}") + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( Arrays.asList(