Add IDEA data-flow analysis to guess nullability

Add "if return..." folding to "return if"
This commit is contained in:
Simon Ogorodnik
2017-05-25 15:54:10 +03:00
parent 1f26353de4
commit e41c027c9a
24 changed files with 261 additions and 40 deletions
@@ -63,6 +63,11 @@ fun KtExpression.unwrapBlockOrParenthesis(): KtExpression {
return innerExpression return innerExpression
} }
fun KtExpression?.isTrivialStatementBody(): Boolean = when (this?.unwrapBlockOrParenthesis()) {
is KtIfExpression, is KtBlockExpression -> false
else -> true
}
fun KtExpression?.isNullExpression(): Boolean = this?.unwrapBlockOrParenthesis()?.node?.elementType == KtNodeTypes.NULL fun KtExpression?.isNullExpression(): Boolean = this?.unwrapBlockOrParenthesis()?.node?.elementType == KtNodeTypes.NULL
fun KtExpression?.isNullExpressionOrEmptyBlock(): Boolean = this.isNullExpression() || this is KtBlockExpression && this.statements.isEmpty() fun KtExpression?.isNullExpressionOrEmptyBlock(): Boolean = this.isNullExpression() || this is KtBlockExpression && this.statements.isEmpty()
@@ -16,8 +16,13 @@
package org.jetbrains.kotlin.idea.j2k package org.jetbrains.kotlin.idea.j2k
import com.intellij.codeInspection.dataFlow.DfaUtil
import com.intellij.codeInspection.dataFlow.Nullness
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiVariable
import org.jetbrains.kotlin.j2k.* import org.jetbrains.kotlin.j2k.*
import org.jetbrains.kotlin.j2k.ast.Nullability
object IdeaJavaToKotlinServices : JavaToKotlinConverterServices { object IdeaJavaToKotlinServices : JavaToKotlinConverterServices {
override val referenceSearcher: ReferenceSearcher override val referenceSearcher: ReferenceSearcher
@@ -31,8 +36,25 @@ object IdeaJavaToKotlinServices : JavaToKotlinConverterServices {
override val docCommentConverter: DocCommentConverter override val docCommentConverter: DocCommentConverter
get() = IdeaDocCommentConverter get() = IdeaDocCommentConverter
override val javaDataFlowAnalyzerFacade: JavaDataFlowAnalyzerFacade
get() = IdeaJavaDataFlowAnalyzerFacade
} }
object IdeaSuperMethodSearcher : SuperMethodsSearcher { object IdeaSuperMethodSearcher : SuperMethodsSearcher {
override fun findDeepestSuperMethods(method: PsiMethod) = method.findDeepestSuperMethods().asList() override fun findDeepestSuperMethods(method: PsiMethod) = method.findDeepestSuperMethods().asList()
}
private object IdeaJavaDataFlowAnalyzerFacade : JavaDataFlowAnalyzerFacade {
override fun variableNullability(variable: PsiVariable, context: PsiElement): Nullability =
DfaUtil.checkNullness(variable, context).toNullability()
override fun methodNullability(method: PsiMethod): Nullability =
DfaUtil.inferMethodNullity(method).toNullability()
private fun Nullness.toNullability() = when (this) {
Nullness.UNKNOWN -> Nullability.Default
Nullness.NOT_NULL -> Nullability.NotNull
Nullness.NULLABLE -> Nullability.Nullable
}
} }
@@ -27,8 +27,11 @@ import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.core.setVisibility import org.jetbrains.kotlin.idea.core.setVisibility
import org.jetbrains.kotlin.idea.inspections.RedundantSamConstructorInspection import org.jetbrains.kotlin.idea.inspections.RedundantSamConstructorInspection
import org.jetbrains.kotlin.idea.intentions.* import org.jetbrains.kotlin.idea.intentions.*
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions.FoldIfToReturnAsymmetricallyIntention
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions.FoldIfToReturnIntention
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions.IfThenToElvisIntention import org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions.IfThenToElvisIntention
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions.IfThenToSafeAccessIntention import org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions.IfThenToSafeAccessIntention
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isTrivialStatementBody
import org.jetbrains.kotlin.idea.intentions.conventionNameCalls.ReplaceGetOrSetInspection import org.jetbrains.kotlin.idea.intentions.conventionNameCalls.ReplaceGetOrSetInspection
import org.jetbrains.kotlin.idea.intentions.conventionNameCalls.ReplaceGetOrSetIntention import org.jetbrains.kotlin.idea.intentions.conventionNameCalls.ReplaceGetOrSetIntention
import org.jetbrains.kotlin.idea.quickfix.RemoveModifierFix import org.jetbrains.kotlin.idea.quickfix.RemoveModifierFix
@@ -76,9 +79,14 @@ object J2KPostProcessingRegistrar {
_processings.add(RemoveRedundantCastToNullableProcessing()) _processings.add(RemoveRedundantCastToNullableProcessing())
registerIntentionBasedProcessing(ConvertToExpressionBodyIntention(convertEmptyToUnit = false)) { it is KtPropertyAccessor } registerIntentionBasedProcessing(ConvertToExpressionBodyIntention(convertEmptyToUnit = false)) { it is KtPropertyAccessor }
registerIntentionBasedProcessing(FoldInitializerAndIfToElvisIntention())
registerIntentionBasedProcessing(FoldIfToReturnIntention()) { it.then.isTrivialStatementBody() && it.`else`.isTrivialStatementBody() }
registerIntentionBasedProcessing(FoldIfToReturnAsymmetricallyIntention()) { it.then.isTrivialStatementBody() && (KtPsiUtil.skipTrailingWhitespacesAndComments(it) as KtReturnExpression).returnedExpression.isTrivialStatementBody() }
registerIntentionBasedProcessing(IfThenToSafeAccessIntention()) registerIntentionBasedProcessing(IfThenToSafeAccessIntention())
registerIntentionBasedProcessing(IfThenToElvisIntention()) registerIntentionBasedProcessing(IfThenToElvisIntention())
registerIntentionBasedProcessing(FoldInitializerAndIfToElvisIntention())
registerIntentionBasedProcessing(SimplifyNegatedBinaryExpressionIntention()) registerIntentionBasedProcessing(SimplifyNegatedBinaryExpressionIntention())
registerIntentionBasedProcessing(ReplaceGetOrSetIntention(), additionalChecker = ReplaceGetOrSetInspection.additionalChecker) registerIntentionBasedProcessing(ReplaceGetOrSetIntention(), additionalChecker = ReplaceGetOrSetInspection.additionalChecker)
registerIntentionBasedProcessing(AddOperatorModifierIntention()) registerIntentionBasedProcessing(AddOperatorModifierIntention())
@@ -23,6 +23,7 @@ interface JavaToKotlinConverterServices {
val superMethodsSearcher: SuperMethodsSearcher val superMethodsSearcher: SuperMethodsSearcher
val resolverForConverter: ResolverForConverter val resolverForConverter: ResolverForConverter
val docCommentConverter: DocCommentConverter val docCommentConverter: DocCommentConverter
val javaDataFlowAnalyzerFacade: JavaDataFlowAnalyzerFacade
} }
object EmptyJavaToKotlinServices: JavaToKotlinConverterServices { object EmptyJavaToKotlinServices: JavaToKotlinConverterServices {
@@ -37,6 +38,9 @@ object EmptyJavaToKotlinServices: JavaToKotlinConverterServices {
override val docCommentConverter: DocCommentConverter override val docCommentConverter: DocCommentConverter
get() = EmptyDocCommentConverter get() = EmptyDocCommentConverter
override val javaDataFlowAnalyzerFacade: JavaDataFlowAnalyzerFacade
get() = JavaDataFlowAnalyzerFacade.Default
} }
interface SuperMethodsSearcher { interface SuperMethodsSearcher {
@@ -27,6 +27,19 @@ import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.types.TypeUtils import org.jetbrains.kotlin.types.TypeUtils
import java.util.* import java.util.*
interface JavaDataFlowAnalyzerFacade {
fun variableNullability(variable: PsiVariable, context: PsiElement): Nullability
fun methodNullability(method: PsiMethod): Nullability
object Default : JavaDataFlowAnalyzerFacade {
override fun methodNullability(method: PsiMethod): Nullability = Nullability.Default
override fun variableNullability(variable: PsiVariable, context: PsiElement): Nullability = Nullability.Default
}
}
class TypeConverter(val converter: Converter) { class TypeConverter(val converter: Converter) {
private val typesBeingConverted = HashSet<PsiType>() private val typesBeingConverted = HashSet<PsiType>()
@@ -74,6 +87,9 @@ class TypeConverter(val converter: Converter) {
fun variableNullability(variable: PsiVariable): Nullability fun variableNullability(variable: PsiVariable): Nullability
= nullabilityFlavor.forVariableType(variable, true) = nullabilityFlavor.forVariableType(variable, true)
fun variableReferenceNullability(variable: PsiVariable, reference: PsiReferenceExpression): Nullability
= nullabilityFlavor.forVariableReference(variable, reference)
fun methodNullability(method: PsiMethod): Nullability fun methodNullability(method: PsiMethod): Nullability
= nullabilityFlavor.forMethodReturnType(method) = nullabilityFlavor.forMethodReturnType(method)
@@ -181,6 +197,8 @@ class TypeConverter(val converter: Converter) {
return value return value
} }
abstract fun fromDataFlowForMethod(method: PsiMethod): T
private fun forMethodReturnTypeNoCache(method: PsiMethod): T { private fun forMethodReturnTypeNoCache(method: PsiMethod): T {
val returnType = method.returnType ?: return default val returnType = method.returnType ?: return default
@@ -197,6 +215,9 @@ class TypeConverter(val converter: Converter) {
value = fromTypeHeuristics(returnType) value = fromTypeHeuristics(returnType)
if (value != default) return value if (value != default) return value
value = fromDataFlowForMethod(method)
if (value != default) return value
if (!converter.inConversionScope(method)) return default // do not analyze body and usages of methods out of our conversion scope if (!converter.inConversionScope(method)) return default // do not analyze body and usages of methods out of our conversion scope
val body = method.body val body = method.body
@@ -227,6 +248,17 @@ class TypeConverter(val converter: Converter) {
} }
private val nullabilityFlavor = object : TypeFlavor<Nullability>(Nullability.Default) { private val nullabilityFlavor = object : TypeFlavor<Nullability>(Nullability.Default) {
fun forVariableReference(variable: PsiVariable, reference: PsiReferenceExpression): Nullability {
assert(reference.resolve() == variable)
val dataFlowUtil = converter.services.javaDataFlowAnalyzerFacade
return dataFlowUtil.variableNullability(variable, reference).takeIf { it != default } ?:
variableNullability(variable)
}
override fun fromDataFlowForMethod(method: PsiMethod): Nullability =
converter.services.javaDataFlowAnalyzerFacade.methodNullability(method)
override val forEnumConstant: Nullability override val forEnumConstant: Nullability
get() = Nullability.NotNull get() = Nullability.NotNull
@@ -361,7 +393,9 @@ class TypeConverter(val converter: Converter) {
is PsiParenthesizedExpression -> expression?.nullability() ?: Nullability.Default is PsiParenthesizedExpression -> expression?.nullability() ?: Nullability.Default
is PsiCallExpression -> resolveMethod()?.let { methodNullability(it) } ?: Nullability.Default
is PsiReferenceExpression -> (resolve() as? PsiVariable)?.let { variableReferenceNullability(it, this) } ?: Nullability.Default
//TODO: some other cases //TODO: some other cases
else -> Nullability.Default else -> Nullability.Default
@@ -370,6 +404,8 @@ class TypeConverter(val converter: Converter) {
} }
private val mutabilityFlavor = object : TypeFlavor<Mutability>(Mutability.Default) { private val mutabilityFlavor = object : TypeFlavor<Mutability>(Mutability.Default) {
override fun fromDataFlowForMethod(method: PsiMethod): Mutability = Mutability.Default
override val forEnumConstant: Mutability get() = Mutability.NonMutable override val forEnumConstant: Mutability get() = Mutability.NonMutable
override fun fromType(type: PsiType): Mutability { override fun fromType(type: PsiType): Mutability {
@@ -3,7 +3,7 @@
internal class Library { internal class Library {
fun call() {} fun call() {}
val string: String? val string: String
get() = "" get() = ""
} }
@@ -11,9 +11,9 @@ internal class User {
fun main() { fun main() {
val lib: Library = Library() val lib: Library = Library()
lib.call() lib.call()
lib.string!!.isEmpty() lib.string.isEmpty()
Library().call() Library().call()
Library().string!!.isEmpty() Library().string.isEmpty()
} }
} }
@@ -1,7 +1,6 @@
// ERROR: Type mismatch: inferred type is String? but Any was expected
internal class A { internal class A {
private val s: String? = null private val s: String? = null
val value: Any val value: Any?
get() = s get() = s
} }
+1 -1
View File
@@ -1 +1 @@
if (true) return 1 else return 0 return if (true) 1 else 0
+3 -3
View File
@@ -11,9 +11,9 @@ object Test {
return false return false
} }
val result = true val result = true
if (parent.isDirectory) { return if (parent.isDirectory) {
return true true
} else } else
return false false
} }
} }
+1 -3
View File
@@ -8,9 +8,7 @@ internal class Test {
if (s1.isEmpty() && s2.isEmpty()) if (s1.isEmpty() && s2.isEmpty())
return "OK" return "OK"
if (s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) return if (s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) "OOOK" else ""
return "OOOK"
return ""
} }
} }
@@ -1,9 +1,9 @@
internal class C { internal class C {
fun foo(b: Boolean): String? { fun foo(b: Boolean): String? {
if (b) { return if (b) {
return "abc" "abc"
} else { } else {
return null null
} }
} }
} }
@@ -1,5 +1,4 @@
fun foo(s: String?, b: Boolean): Int { fun foo(s: String?, b: Boolean): Int {
if (s == null) println("null") if (s == null) println("null")
if (b) return s!!.length return if (b) s!!.length else 10
return 10
} }
+1 -1
View File
@@ -25,7 +25,7 @@ internal class C : Base(), I {
return "" return ""
} }
override fun zoo(o: Any?): String? { override fun zoo(o: Any?): String {
return "" return ""
} }
} }
+1 -4
View File
@@ -1,8 +1,5 @@
internal class A { internal class A {
fun foo(s: String?): Int { fun foo(s: String?): Int {
if (s != null) { return s?.length ?: -1
return s.length
}
return -1
} }
} }
+1 -2
View File
@@ -1,4 +1,3 @@
// ERROR: Type mismatch: inferred type is String? but String was expected
// ERROR: Type mismatch: inferred type is Int? but Int was expected // ERROR: Type mismatch: inferred type is Int? but Int was expected
// ERROR: Type inference failed. Please try to specify type arguments explicitly. // ERROR: Type inference failed. Please try to specify type arguments explicitly.
// ERROR: Using 'remove(Int): T' is an error. Use removeAt(index) instead. // ERROR: Using 'remove(Int): T' is an error. Use removeAt(index) instead.
@@ -50,7 +49,7 @@ class Test {
} }
fun foo2(s: String) { fun foo2(s: String?) {
} }
@@ -1,20 +1,18 @@
// ERROR: Type mismatch: inferred type is String? but String was expected
// ERROR: Type mismatch: inferred type is String? but String was expected
class Test { class Test {
fun nullableString(p: Int): String? { fun nullableString(p: Int): String? {
return if (p > 0) "response" else null return if (p > 0) "response" else null
} }
private var nullableInitializerField: String = nullableString(3) private var nullableInitializerField = nullableString(3)
private val nullableInitializerFieldFinal = nullableString(3) private val nullableInitializerFieldFinal = nullableString(3)
var nullableInitializerPublicField: String = nullableString(3) var nullableInitializerPublicField = nullableString(3)
fun testProperty() { fun testProperty() {
nullableInitializerField = "aaa" nullableInitializerField = "aaa"
nullableInitializerField[0] nullableInitializerField!![0]
nullableInitializerFieldFinal!![0] nullableInitializerFieldFinal!![0]
nullableInitializerPublicField[0] nullableInitializerPublicField!![0]
} }
fun testLocalVariable() { fun testLocalVariable() {
+3 -3
View File
@@ -1,9 +1,9 @@
fun foo(i: Int, j: Int): String { fun foo(i: Int, j: Int): String {
when (i) { when (i) {
0 -> if (j > 0) { 0 -> return if (j > 0) {
return "1" "1"
} else { } else {
return "2" "2"
} }
1 -> return "3" 1 -> return "3"
else -> return "4" else -> return "4"
+3 -4
View File
@@ -1,10 +1,9 @@
fun foo(i: Int, j: Int): String { fun foo(i: Int, j: Int): String {
when (i) { when (i) {
0 -> { 0 -> {
if (j > 0) { return if (j > 0) {
return "1" "1"
} } else "2"
return "2"
} }
1 -> return "2" 1 -> return "2"
else -> return "3" else -> return "3"
@@ -1,7 +1,6 @@
internal class A { internal class A {
private fun foo(o: Any?, b: Boolean): String? { private fun foo(o: Any?, b: Boolean): String? {
if (b) return o as String? return if (b) o as String? else ""
return ""
} }
fun bar() { fun bar() {
@@ -0,0 +1,53 @@
public class SomeServiceUsage {
public SomeService getService() {
return SomeService.getInstanceNotNull();
}
public SomeService getServiceNullable() {
return SomeService.getInstanceNullable();
}
// elvis
public SomeService getServiceNotNullByDataFlow() {
SomeService s = SomeService.getInstanceNullable();
return s == null ? SomeService.getInstanceNotNull() : s;
}
// nullable, bang-bang
public String aString1() {
return getServiceNullable().nullableString();
}
// nullable
public String aString2() {
return getService().nullableString();
}
// not nullable
public String aString3() {
return getService().notNullString();
}
// nullable, no bang-bang
public String aString4() {
return getServiceNotNullByDataFlow().nullableString();
}
// not nullable, no bang-bang
public String aString5() {
return getServiceNotNullByDataFlow().notNullString();
}
// nullable, safe-call
public String aString6() {
SomeService s = getServiceNullable();
if (s != null) {
return s.nullableString();
} else {
return null;
}
}
}
@@ -0,0 +1,47 @@
class SomeServiceUsage {
val service: SomeService
get() = SomeService.getInstanceNotNull()
val serviceNullable: SomeService?
get() = SomeService.getInstanceNullable()
// elvis
val serviceNotNullByDataFlow: SomeService
get() {
val s = SomeService.getInstanceNullable()
return s ?: SomeService.getInstanceNotNull()
}
// nullable, bang-bang
fun aString1(): String? {
return serviceNullable!!.nullableString()
}
// nullable
fun aString2(): String? {
return service.nullableString()
}
// not nullable
fun aString3(): String {
return service.notNullString()
}
// nullable, no bang-bang
fun aString4(): String? {
return serviceNotNullByDataFlow.nullableString()
}
// not nullable, no bang-bang
fun aString5(): String {
return serviceNotNullByDataFlow.notNullString()
}
// nullable, safe-call
fun aString6(): String? {
val s = serviceNullable
return s?.nullableString()
}
}
@@ -0,0 +1,26 @@
public class SomeService {
public static SomeService getInstanceNotNull() {
return new SomeService();
}
public static SomeService getInstanceNullable() {
if (Math.random() > 0.5)
return null;
return new SomeService();
}
public String nullableString() {
if (Math.random() < 0.5)
return null;
return Math.random() + "";
}
public String notNullString() {
String s = nullableString();
if (s != null)
return s;
return "null";
}
}
@@ -0,0 +1,26 @@
public class SomeService {
public static SomeService getInstanceNotNull() {
return new SomeService();
}
public static SomeService getInstanceNullable() {
if (Math.random() > 0.5)
return null;
return new SomeService();
}
public String nullableString() {
if (Math.random() < 0.5)
return null;
return Math.random() + "";
}
public String notNullString() {
String s = nullableString();
if (s != null)
return s;
return "null";
}
}
@@ -66,6 +66,12 @@ public class JavaToKotlinConverterMultiFileTestGenerated extends AbstractJavaToK
doTest(fileName); doTest(fileName);
} }
@TestMetadata("NullabilityByDFa")
public void testNullabilityByDFa() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("j2k/testData/multiFile/NullabilityByDFa/");
doTest(fileName);
}
@TestMetadata("ProtectedVisibility") @TestMetadata("ProtectedVisibility")
public void testProtectedVisibility() throws Exception { public void testProtectedVisibility() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("j2k/testData/multiFile/ProtectedVisibility/"); String fileName = KotlinTestUtils.navigationMetadata("j2k/testData/multiFile/ProtectedVisibility/");