Java to Kotlin converter: even more smartness about nullability

This commit is contained in:
Valentin Kipyatkov
2014-06-04 17:35:46 +04:00
parent 15ffb6768c
commit d49b4f86fd
29 changed files with 273 additions and 50 deletions
@@ -18,7 +18,6 @@ package org.jetbrains.jet.j2k
import com.intellij.psi.*
import java.util.LinkedHashSet
import com.intellij.psi.util.PsiUtil
fun PsiMethod.isPrimaryConstructor(): Boolean {
if (!isConstructor()) return false
@@ -63,19 +62,21 @@ fun PsiClass.getPrimaryConstructor(): PsiMethod? {
fun PsiElement.isInsidePrimaryConstructor(): Boolean
= getContainingConstructor()?.isPrimaryConstructor() ?: false
fun PsiElement.getContainingConstructor(): PsiMethod? {
fun PsiElement.getContainingMethod(): PsiMethod? {
var context = getContext()
while (context != null) {
val _context = context!!
if (_context is PsiMethod) {
return if (_context.isConstructor()) _context else null
}
if (_context is PsiMethod) return _context
context = _context.getContext()
}
return null
}
fun PsiElement.getContainingConstructor(): PsiMethod? {
val method = getContainingMethod()
return if (method?.isConstructor() == true) method else null
}
fun PsiMethodCallExpression.isSuperConstructorCall(): Boolean {
val ref = getMethodExpression()
if (ref.getCanonicalText() == "super") {
+54 -8
View File
@@ -315,16 +315,28 @@ public class Converter(val project: Project, val settings: ConverterSettings) {
comments: MemberComments,
membersToRemove: MutableSet<PsiMember>): PrimaryConstructor {
val params = constructor.getParameterList().getParameters()
val parameterToField = HashMap<PsiParameter, PsiField>()
val parameterToField = HashMap<PsiParameter, Pair<PsiField, Type>>()
val body = constructor.getBody()
val block = if (body != null) {
val statementsToRemove = HashSet<PsiStatement>()
val usageReplacementMap = HashMap<PsiVariable, String>()
for (parameter in params) {
val (field, initializationStatement) = findBackingFieldForConstructorParameter(parameter, constructor) ?: continue
if (convertVariableType(field) != convertVariableType(parameter)) continue
parameterToField.put(parameter, field)
val fieldType = convertVariableType(field)
val parameterType = convertVariableType(parameter)
// types can be different only in nullability
val `type` = if (fieldType == parameterType) {
fieldType
}
else if (fieldType.toNotNullType() == parameterType.toNotNullType()) {
if (fieldType.isNullable) fieldType else parameterType // prefer nullable one
}
else {
continue
}
parameterToField.put(parameter, field to `type`)
statementsToRemove.add(initializationStatement)
membersToRemove.add(field)
@@ -345,13 +357,13 @@ public class Converter(val project: Project, val settings: ConverterSettings) {
}
val parameterList = ParameterList(params.map {
val field = parameterToField[it]
if (field == null) {
if (!parameterToField.containsKey(it)) {
convertParameter(it)
}
else {
val (field, `type`) = parameterToField[it]!!
Parameter(Identifier(field.getName()!!),
convertVariableType(it),
`type`,
if (field.hasModifierProperty(PsiModifier.FINAL)) Parameter.VarValModifier.Val else Parameter.VarValModifier.Var,
convertModifiers(field).filter { ACCESS_MODIFIERS.contains(it) })
}
@@ -362,7 +374,7 @@ public class Converter(val project: Project, val settings: ConverterSettings) {
private fun findBackingFieldForConstructorParameter(parameter: PsiParameter, constructor: PsiMethod): Pair<PsiField, PsiStatement>? {
val body = constructor.getBody() ?: return null
val refs = findExpressionReferences(parameter, body)
val refs = findVariableReferences(parameter, body)
if (refs.any { PsiUtil.isAccessedForWriting(it) }) return null
@@ -381,7 +393,7 @@ public class Converter(val project: Project, val settings: ConverterSettings) {
if (statement.getParent() != body) continue
// and no other assignments to field should exist in the constructor
if (findExpressionReferences(field, body).any { it != assignee && PsiUtil.isAccessedForWriting(it) && isQualifierEmptyOrThis(it) }) continue
if (findVariableReferences(field, body).any { it != assignee && PsiUtil.isAccessedForWriting(it) && isQualifierEmptyOrThis(it) }) continue
//TODO: check access to field before assignment
return field to statement
@@ -459,6 +471,7 @@ public class Converter(val project: Project, val settings: ConverterSettings) {
public fun convertVariableType(variable: PsiVariable): Type {
var nullability = variable.nullabilityFromAnnotations()
if (nullability == Nullability.Default) {
val initializer = variable.getInitializer()
if (initializer != null) {
@@ -471,9 +484,28 @@ public class Converter(val project: Project, val settings: ConverterSettings) {
}
}
}
if (nullability == Nullability.Default) {
val scope = usageScope(variable)
if (scope != null) {
if (findVariableReferences(variable, scope).any { isVariableNullableFromUsage(it) }) {
nullability = Nullability.Nullable
}
}
}
return convertType(variable.getType(), nullability)
}
private fun usageScope(variable: PsiVariable): PsiElement? {
return when(variable) {
is PsiParameter -> variable.getDeclarationScope()
is PsiField -> if (variable.hasModifierProperty(PsiModifier.PRIVATE)) variable.getContainingClass() else variable.getContainingFile()
is PsiLocalVariable -> variable.getContainingMethod()
else -> null
}
}
private fun PsiExpression.nullability(): Nullability {
return when (this) {
is PsiLiteralExpression -> if (getValue() != null) Nullability.NotNull else Nullability.Nullable
@@ -497,6 +529,20 @@ public class Converter(val project: Project, val settings: ConverterSettings) {
}
}
private fun isVariableNullableFromUsage(ref: PsiReferenceExpression): Boolean {
val parent = ref.getParent() ?: return false
if (parent is PsiAssignmentExpression && parent.getOperationTokenType() == JavaTokenType.EQ && ref == parent.getLExpression()) {
return parent.getRExpression()?.nullability() == Nullability.Nullable
}
else if (parent is PsiBinaryExpression) {
val operationType = parent.getOperationTokenType()
if (operationType == JavaTokenType.EQEQ || operationType == JavaTokenType.NE) {
val otherOperand = if (ref == parent.getLOperand()) parent.getROperand() else parent.getLOperand()
return otherOperand?.nullability() == Nullability.Nullable
}
}
return false
}
private fun convertToNotNullableTypes(types: Array<out PsiType?>): List<Type>
= types.map { convertType(it, Nullability.NotNull) }
+6 -12
View File
@@ -17,28 +17,22 @@
package org.jetbrains.jet.j2k
import org.jetbrains.jet.j2k.ast.Identifier
import com.intellij.psi.PsiElement
import com.intellij.psi.JavaRecursiveElementVisitor
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.PsiNewExpression
import org.jetbrains.jet.j2k.ast.Field
import org.jetbrains.jet.lang.types.expressions.OperatorConventions
import com.intellij.psi.PsiModifierListOwner
import java.util.ArrayList
import com.intellij.psi.util.PsiUtil
import com.intellij.psi.PsiThisExpression
import org.jetbrains.jet.j2k.ast.Nullability
import com.intellij.psi.*
import com.intellij.psi.util.PsiUtil
fun quoteKeywords(packageName: String): String = packageName.split("\\.").map { Identifier(it).toKotlin() }.makeString(".")
fun findExpressionReferences(element: PsiElement, scope: PsiElement): Collection<PsiReferenceExpression> {
fun findVariableReferences(variable: PsiVariable, scope: PsiElement): Collection<PsiReferenceExpression> {
class Visitor : JavaRecursiveElementVisitor() {
val refs = ArrayList<PsiReferenceExpression>()
override fun visitReferenceExpression(expression: PsiReferenceExpression) {
super.visitReferenceExpression(expression)
if (expression.isReferenceTo(element)) {
if (expression.isReferenceTo(variable)) {
refs.add(expression)
}
}
@@ -49,8 +43,8 @@ fun findExpressionReferences(element: PsiElement, scope: PsiElement): Collection
return visitor.refs
}
fun PsiElement.countWriteAccesses(scope: PsiElement?): Int
= if (scope != null) findExpressionReferences(this, scope).count { PsiUtil.isAccessedForWriting(it) } else 0
fun PsiVariable.countWriteAccesses(scope: PsiElement?): Int
= if (scope != null) findVariableReferences(this, scope).count { PsiUtil.isAccessedForWriting(it) } else 0
fun PsiModifierListOwner.nullabilityFromAnnotations(): Nullability {
val annotations = getModifierList()?.getAnnotations() ?: return Nullability.Default
@@ -19,20 +19,25 @@ package org.jetbrains.jet.j2k.ast
import org.jetbrains.jet.j2k.ConverterSettings
class LocalVariable(
val identifier: Identifier,
val modifiersSet: Set<Modifier>,
val javaType: Type,
val initializer: Expression,
val isVal: Boolean,
val settings: ConverterSettings
private val identifier: Identifier,
private val modifiers: Set<Modifier>,
private val typeCalculator: () -> Type /* we use lazy type calculation for better performance */,
private val initializer: Expression,
private val isVal: Boolean,
private val settings: ConverterSettings
) : Expression() {
override fun toKotlin(): String {
if (initializer.isEmpty) {
return "${identifier.toKotlin()} : ${javaType.toKotlin()}"
val varVal = if (isVal) "val" else "var"
return if (initializer.isEmpty) {
"$varVal ${identifier.toKotlin()} : ${typeCalculator().toKotlin()}"
}
else {
val shouldSpecifyType = settings.specifyLocalVariableTypeByDefault
if (shouldSpecifyType)
"$varVal ${identifier.toKotlin()} : ${typeCalculator().toKotlin()} = ${initializer.toKotlin()}"
else
"$varVal ${identifier.toKotlin()} = ${initializer.toKotlin()}"
}
val shouldSpecifyType = settings.specifyLocalVariableTypeByDefault
return "${identifier.toKotlin()} ${if (shouldSpecifyType) ": ${javaType.toKotlin()} " else ""}= ${initializer.toKotlin()}"
}
}
@@ -28,10 +28,7 @@ abstract class Statement() : Element {
open class DeclarationStatement(val elements: List<Element>) : Statement() {
override fun toKotlin(): String
= elements.filterIsInstance(javaClass<LocalVariable>()).map { convertDeclaration(it) }.makeString("\n")
private fun convertDeclaration(v: LocalVariable): String
= (if (v.isVal) "val" else "var") + " " + v.toKotlin()
= elements.filterIsInstance(javaClass<LocalVariable>()).map { it.toKotlin() }.makeString("\n")
}
open class ExpressionListStatement(val expressions: List<Expression>) : Expression() {
@@ -53,7 +50,7 @@ open class IfStatement(
) : Expression() {
override fun toKotlin(): String {
val result: String = "if (" + condition.toKotlin() + ")\n" + thenStatement.toKotlin()
if (elseStatement != Statement.Empty) {
if (!elseStatement.isEmpty) {
return result + "\nelse\n" + elseStatement.toKotlin()
}
@@ -69,6 +69,8 @@ trait Type : Element {
override fun equals(other: Any?): Boolean = other is Type && other.toKotlin() == this.toKotlin()
override fun hashCode(): Int = toKotlin().hashCode()
override fun toString(): String = toKotlin()
}
open class ClassType(val `type`: Identifier, val parameters: List<Element>, nullability: Nullability, converter: Converter)
@@ -27,7 +27,7 @@ class ElementVisitor(public val converter: Converter) : JavaElementVisitor() {
override fun visitLocalVariable(variable: PsiLocalVariable) {
result = LocalVariable(Identifier(variable.getName()!!),
converter.convertModifiers(variable),
converter.convertVariableType(variable),
{ converter.convertVariableType(variable) },
converter.convertExpression(variable.getInitializer(), variable.getType()),
converter.settings.forceLocalVariableImmutability || variable.hasModifierProperty(PsiModifier.FINAL),
converter.settings)
@@ -1780,19 +1780,59 @@ public class JavaToKotlinConverterTestGenerated extends AbstractJavaToKotlinConv
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.TestsPackage", new File("j2k/tests/testData/ast/nullability"), Pattern.compile("^(.+)\\.java$"), true);
}
@TestMetadata("FieldAssignedWithNull.java")
public void testFieldAssignedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/FieldAssignedWithNull.java");
}
@TestMetadata("FieldComparedWithNull.java")
public void testFieldComparedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/FieldComparedWithNull.java");
}
@TestMetadata("FieldComparedWithNull2.java")
public void testFieldComparedWithNull2() throws Exception {
doTest("j2k/tests/testData/ast/nullability/FieldComparedWithNull2.java");
}
@TestMetadata("FieldComparedWithNull3.java")
public void testFieldComparedWithNull3() throws Exception {
doTest("j2k/tests/testData/ast/nullability/FieldComparedWithNull3.java");
}
@TestMetadata("FieldComparedWithNull4.java")
public void testFieldComparedWithNull4() throws Exception {
doTest("j2k/tests/testData/ast/nullability/FieldComparedWithNull4.java");
}
@TestMetadata("FieldInitializedWithNull.java")
public void testFieldInitializedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/FieldInitializedWithNull.java");
}
@TestMetadata("InitializedWithNull.java")
public void testInitializedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/InitializedWithNull.java");
@TestMetadata("ParameterComparedWithNull.java")
public void testParameterComparedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/ParameterComparedWithNull.java");
}
@TestMetadata("InitializedWithTernaryNull.java")
public void testInitializedWithTernaryNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/InitializedWithTernaryNull.java");
@TestMetadata("VariableAssignedWithNull.java")
public void testVariableAssignedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/VariableAssignedWithNull.java");
}
@TestMetadata("VariableComparedWithNull.java")
public void testVariableComparedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/VariableComparedWithNull.java");
}
@TestMetadata("VariableInitializedWithNull.java")
public void testVariableInitializedWithNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/VariableInitializedWithNull.java");
}
@TestMetadata("VariableInitializedWithTernaryNull.java")
public void testVariableInitializedWithTernaryNull() throws Exception {
doTest("j2k/tests/testData/ast/nullability/VariableInitializedWithTernaryNull.java");
}
}
@@ -7,7 +7,7 @@ import java.io.File
*/
public class Test() {
class object {
public fun isDir(parent: File): Boolean {
public fun isDir(parent: File?): Boolean {
if (parent == null || !parent.exists()) {
return false
}
@@ -0,0 +1,8 @@
//file
class C {
private String s = "";
void foo() {
s = null
}
}
@@ -0,0 +1,7 @@
class C() {
private var s: String? = ""
fun foo() {
s = null
}
}
@@ -0,0 +1,10 @@
//file
class C {
private String s = "";
void foo() {
if (s == null) {
System.out.print("null");
}
}
}
@@ -0,0 +1,9 @@
class C() {
private var s: String? = ""
fun foo() {
if (s == null) {
System.out.print("null")
}
}
}
@@ -0,0 +1,14 @@
//file
class C {
private String s;
public C(String s) {
this.s = s;
}
void foo() {
if (s != null) {
System.out.print("not null");
}
}
}
@@ -0,0 +1,8 @@
class C(private var s: String?) {
fun foo() {
if (s != null) {
System.out.print("not null")
}
}
}
@@ -0,0 +1,12 @@
//file
class C {
public String s = "";
}
class D {
void foo(C c) {
if (null == c.s) {
System.out.println("null");
}
}
}
@@ -0,0 +1,11 @@
class C() {
public var s: String? = ""
}
class D() {
fun foo(c: C) {
if (null == c.s) {
System.out.println("null")
}
}
}
@@ -0,0 +1,11 @@
//file
class C {
private String s;
public C(String s) {
this.s = s;
if (s == null) {
System.out.print("null");
}
}
}
@@ -0,0 +1,7 @@
class C(private var s: String?) {
{
if (s == null) {
System.out.print("null")
}
}
}
@@ -0,0 +1,6 @@
//method
void foo(String s) {
if (s != null) {
zoo(s)
}
}
@@ -0,0 +1,5 @@
fun foo(s: String?) {
if (s != null) {
zoo(s)
}
}
@@ -0,0 +1,8 @@
//method
// !specifyLocalVariableTypeByDefault: true
void foo(boolean b) {
String s = "abc";
if (b) {
s = null
}
}
@@ -0,0 +1,7 @@
// !specifyLocalVariableTypeByDefault: true
fun foo(b: Boolean) {
val s: String? = "abc"
if (b) {
s = null
}
}
@@ -0,0 +1,8 @@
//method
// !specifyLocalVariableTypeByDefault: true
void foo() {
String s = bar()
if (s != null) {
zoo(s)
}
}
@@ -0,0 +1,7 @@
// !specifyLocalVariableTypeByDefault: true
fun foo() {
val s: String? = bar()
if (s != null) {
zoo(s)
}
}