From 14d3693a6fc15967562c3aba46d0a6f9761244f0 Mon Sep 17 00:00:00 2001 From: NikitaAware Date: Tue, 7 Feb 2023 10:01:28 -0800 Subject: [PATCH] JSpecify: initial checks for generic type compatibility at assignments (#715) This pull request adds the identical type parameter nullability checks for both sides of an assignment. for example `NullableTypeParam<@Nullable String> nullableTypeParam = new NullableTypeParam();` should generate an error as the type parameters of both sides of the assignment do not have the same Nullability annotations. This PR covers the following cases: - checks for the variable instantiation `A a = new A<@Nullable String>();` - checks for the assignments `A<@Nullable String> t1 = new A<@Nullable String>(); A t2; t2 = t1;` - LHS and RHS with the exact same types `A a = new A<@Nullable String>();` - RHS being a subtype of LHS ```java class Test { class D

{} class B

extends D

{} void test1() { // BUG: Diagnostic contains: Cannot assign from type D f = new B<@Nullable String>(); } } ``` - nested generics `A, String> a = new A>();` The PR does not handle pseudo-assignments due to parameter passing / returns; that support will come in a follow-up PR. Also note that this PR only changes NullAway behavior when `JSpecifyMode` is enabled. Additionally, we don't currently have functioning support for checking compatibility of method references, lambdas, or inferred generic instantiations, but this PR includes tests to show that those cases do not produce false positive errors (just false negatives / missed detections). --- .../java/com/uber/nullaway/ErrorMessage.java | 1 + .../com/uber/nullaway/GenericsChecks.java | 220 ++++++++- .../main/java/com/uber/nullaway/NullAway.java | 9 + .../NullAwayJSpecifyGenericsTests.java | 429 ++++++++++++++---- 4 files changed, 577 insertions(+), 82 deletions(-) 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(