Java to Kotlin converter: even more smartness about nullability
This commit is contained in:
@@ -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") {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user