diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java index 94db423663..432d53e939 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java @@ -28,6 +28,9 @@ public Boolean visitClassType(Type.ClassType lhsType, Type rhsType) { // TODO Handle wildcard types return true; } + if (lhsType.isIntersection()) { + return handleIntersectionType((Type.IntersectionClassType) lhsType, rhsType); + } 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. @@ -64,6 +67,13 @@ public Boolean visitClassType(Type.ClassType lhsType, Type rhsType) { return lhsType.getEnclosingType().accept(this, rhsType.getEnclosingType()); } + /** Check identical nullability for every type in the intersection */ + private Boolean handleIntersectionType( + Type.IntersectionClassType intersectionType, Type rhsType) { + return intersectionType.getBounds().stream() + .allMatch(type -> ((Type) type).accept(this, rhsType)); + } + @Override public Boolean visitArrayType(Type.ArrayType lhsType, Type rhsType) { if (rhsType instanceof NullType) { diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java index b6c2f9c991..f0f5f5b4cc 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java @@ -37,6 +37,9 @@ public String visitWildcardType(Type.WildcardType t, Void unused) { @Override public String visitClassType(Type.ClassType t, Void s) { + if (t.isIntersection()) { + return prettyIntersectionType((Type.IntersectionClassType) t); + } StringBuilder sb = new StringBuilder(); Type enclosingType = t.getEnclosingType(); if (!ASTHelpers.isSameType(enclosingType, Type.noType, state)) { @@ -57,6 +60,12 @@ public String visitClassType(Type.ClassType t, Void s) { return sb.toString(); } + private String prettyIntersectionType(Type.IntersectionClassType t) { + return t.getBounds().stream() + .map(type -> ((Type) type).accept(this, null)) + .collect(joining(" & ")); + } + @Override public String visitCapturedType(Type.CapturedType t, Void s) { return t.wildcard.accept(this, null); diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java index 6db6675078..869f736790 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java @@ -1856,6 +1856,56 @@ public void issue1008() { .doTest(); } + @Test + public void intersectionTypeFromConditionalExprInStringConcat() { + makeHelper() + .addSourceLines( + "ServiceExtraInfo.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "public class ServiceExtraInfo {", + " private java.util.@Nullable List relativeServices;", + " private String getStr() {", + " return (relativeServices == null ? \"relativeServices == null\" : relativeServices.size()) + \"\";", + " }", + " private String getStr2(boolean b, java.util.List l) {", + " return (b ? (b ? l.size() : \"hello\") : 3) + \"\";", + " }", + "}") + .doTest(); + } + + @Test + public void intersectionTypeInvalidAssign() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "import java.io.Serializable;", + "public class Test {", + " interface A {}", + " static class B implements A<@Nullable String>, Serializable {}", + " static class C implements A, Serializable {}", + " static void test1(Object o) {", + " var x = (A & Serializable) o;", + " // BUG: Diagnostic contains: Cannot assign from type B to type A & Serializable", + " x = new B();", + " // ok", + " x = new C();", + " }", + " static void test2(Object o) {", + " var x = (A<@Nullable String> & Serializable) o;", + // TODO: should _not_ be an error, see https://github.com/uber/NullAway/issues/1022 + " // BUG: Diagnostic contains: Cannot assign from type B to type A & Serializable", + " x = new B();", + // TODO: _should_ be an error, see https://github.com/uber/NullAway/issues/1022 + " x = new C();", + " }", + "}") + .doTest(); + } + @Test public void issue1014() { makeHelper()