J2K: Special conversion for java.lang.String methods.

#KT-7732
This commit is contained in:
Ilya Gorbunov
2015-10-02 23:39:13 +03:00
parent 4b92bd100c
commit d7da8b7bd3
7 changed files with 392 additions and 10 deletions
@@ -60,6 +60,9 @@ class CodeConverter(
public fun convertExpressions(expressions: Array<PsiExpression>): List<Expression>
= expressions.map { convertExpression(it) }
public fun convertExpressions(expressions: List<PsiExpression>): List<Expression>
= expressions.map { convertExpression(it) }
public fun convertExpression(expression: PsiExpression?): Expression {
if (expression == null) return Expression.Empty
+131 -10
View File
@@ -16,11 +16,9 @@
package org.jetbrains.kotlin.j2k
import com.intellij.psi.*
import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT
import com.intellij.psi.PsiExpression
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.PsiSuperExpression
import com.intellij.psi.CommonClassNames.JAVA_LANG_STRING
import org.jetbrains.kotlin.j2k.ast.*
enum class SpecialMethod(val qualifiedClassName: String?, val methodName: String, val parameterCount: Int?) {
@@ -71,7 +69,16 @@ enum class SpecialMethod(val qualifiedClassName: String?, val methodName: String
= MethodCallExpression.build(null, "setOf", listOf(codeConverter.convertExpression(arguments.single())), typeArgumentsConverted, false)
},
STRING_REPLACE_ALL("java.lang.String", "replaceAll", 2) {
STRING_TRIM(JAVA_LANG_STRING, "trim", 0) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter): Expression? {
val comparison = BinaryExpression(Identifier("it", isNullable = false).assignNoPrototype(), LiteralExpression("' '").assignNoPrototype(), "<=").assignNoPrototype()
return MethodCallExpression.buildNotNull(
codeConverter.convertExpression(qualifier), "trim",
listOf(LambdaExpression(null, Block.of(comparison).assignNoPrototype())), emptyList())
}
},
STRING_REPLACE_ALL(JAVA_LANG_STRING, "replaceAll", 2) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(codeConverter.convertExpression(qualifier), "replace",
listOf(
@@ -80,6 +87,104 @@ enum class SpecialMethod(val qualifiedClassName: String?, val methodName: String
), emptyList(), false)
},
STRING_REPLACE_FIRST(JAVA_LANG_STRING, "replaceFirst", 2) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(codeConverter.convertExpression(qualifier), "replaceFirst",
listOf(
codeConverter.convertToRegex(arguments[0]),
codeConverter.convertExpression(arguments[1])
), emptyList(), false)
},
STRING_MATCHES(JAVA_LANG_STRING, "matches", 1) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(codeConverter.convertExpression(qualifier), "matches", listOf(codeConverter.convertToRegex(arguments.single())), emptyList(), false)
},
STRING_SPLIT(JAVA_LANG_STRING, "split", 1) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter): Expression? {
val splitCall = MethodCallExpression.buildNotNull(codeConverter.convertExpression(qualifier), "split", listOf(codeConverter.convertToRegex(arguments.single())), emptyList()).assignNoPrototype()
val isEmptyCall = MethodCallExpression.buildNotNull(Identifier("it", isNullable = false).assignNoPrototype(), "isEmpty", emptyList(), emptyList()).assignNoPrototype()
val isEmptyCallBlock = Block.of(isEmptyCall).assignNoPrototype()
val dropLastCall = MethodCallExpression.buildNotNull(splitCall, "dropLastWhile", listOf(LambdaExpression(null, isEmptyCallBlock).assignNoPrototype())).assignNoPrototype()
return MethodCallExpression.buildNotNull(dropLastCall, "toTypedArray", emptyList(), emptyList())
}
},
STRING_SPLIT_LIMIT(JAVA_LANG_STRING, "split", 2) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter): Expression? {
val patternArgument = codeConverter.convertToRegex(arguments[0])
val limitArgument = codeConverter.convertExpression(arguments[1])
val splitArguments =
if (limitArgument is PrefixExpression && limitArgument.op == "-" && limitArgument.expression.let { it is LiteralExpression && it.literalText == "1" })
listOf(patternArgument)
else if (limitArgument is LiteralExpression && limitArgument.literalText.all { it.isDigit() }) {
if (limitArgument.literalText.toInt() == 0) {
return STRING_SPLIT.convertCall(qualifier, arrayOf(arguments[0]), typeArgumentsConverted, codeConverter)
}
listOf(patternArgument, limitArgument)
}
else
listOf(patternArgument, MethodCallExpression.buildNotNull(limitArgument, "coerceAtLeast", listOf(LiteralExpression("0").assignNoPrototype()), emptyList()).assignNoPrototype())
val splitCall = MethodCallExpression.buildNotNull(codeConverter.convertExpression(qualifier), "split", splitArguments, emptyList()).assignNoPrototype()
return MethodCallExpression.buildNotNull(splitCall, "toTypedArray", emptyList(), emptyList())
}
},
STRING_COMPARE_TO_IGNORE_CASE(JAVA_LANG_STRING, "compareToIgnoreCase", 1) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= addIgnoreCaseArgument(qualifier, "compareTo", arguments, typeArgumentsConverted, codeConverter)
},
STRING_EQUALS_IGNORE_CASE(JAVA_LANG_STRING, "equalsIgnoreCase", 1) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= addIgnoreCaseArgument(qualifier, "equals", arguments, typeArgumentsConverted, codeConverter)
},
STRING_REGION_MATCHES(JAVA_LANG_STRING, "regionMatches", 5) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= addIgnoreCaseArgument(qualifier, "regionMatches", arguments.drop(1).toTypedArray(), typeArgumentsConverted, codeConverter, arguments.first())
},
STRING_GET_BYTES(JAVA_LANG_STRING, "getBytes", null) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(codeConverter.convertExpression(qualifier), "toByteArray", codeConverter.convertExpressions(arguments), emptyList(), false)
},
STRING_FORMAT_WITH_LOCALE(JAVA_LANG_STRING, "format", null) {
override fun matches(method: PsiMethod)
= super.matches(method) && method.parameterList.parametersCount >= 2 && method.parameterList.parameters.first().type.canonicalText == "java.util.Locale"
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(codeConverter.convertExpression(arguments[1]), "format", codeConverter.convertExpressions(listOf(arguments[0]) + arguments.drop(2)), emptyList(), false)
},
STRING_FORMAT(JAVA_LANG_STRING, "format", null) {
override fun matches(method: PsiMethod)
= super.matches(method) && method.parameterList.parametersCount >= 1
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(codeConverter.convertExpression(arguments.first()), "format", codeConverter.convertExpressions(arguments.drop(1)), emptyList(), false)
},
STRING_VALUE_OF_CHAR_ARRAY(JAVA_LANG_STRING, "valueOf", null) {
override fun matches(method: PsiMethod)
= matchesClass(method) &&
(matchesName(method) || matchesName(method, "copyValueOf")) &&
method.parameterList.parametersCount.let { it == 1 || it == 3} &&
method.parameterList.parameters.first().type.canonicalText == "char[]"
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(null, "String", codeConverter.convertExpressions(arguments), emptyList(), false)
},
STRING_VALUE_OF(JAVA_LANG_STRING, "valueOf", 1) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= MethodCallExpression.build(codeConverter.convertExpression(arguments.single()), "toString", emptyList(), emptyList(), false)
},
SYSTEM_OUT_PRINTLN("java.io.PrintStream", "println", null) {
override fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter)
= convertSystemOutMethodCall(methodName, qualifier, arguments, typeArgumentsConverted, codeConverter)
@@ -90,11 +195,11 @@ enum class SpecialMethod(val qualifiedClassName: String?, val methodName: String
= convertSystemOutMethodCall(methodName, qualifier, arguments, typeArgumentsConverted, codeConverter)
};
open fun matches(method: PsiMethod): Boolean {
if (method.getName() != methodName) return false
if (qualifiedClassName != null && method.getContainingClass()?.getQualifiedName() != qualifiedClassName) return false
return parameterCount == null || parameterCount == method.getParameterList().getParametersCount()
}
open fun matches(method: PsiMethod): Boolean = matchesName(method) && matchesClass(method) && matchesParameterCount(method)
protected fun matchesName(method: PsiMethod, name: String? = null) = method.name == (name ?: methodName)
protected fun matchesClass(method: PsiMethod) = qualifiedClassName == null || method.containingClass?.qualifiedName == qualifiedClassName
protected fun matchesParameterCount(method: PsiMethod) = parameterCount == null || parameterCount == method.parameterList.parametersCount
abstract fun convertCall(qualifier: PsiExpression?, arguments: Array<PsiExpression>, typeArgumentsConverted: List<Type>, codeConverter: CodeConverter): Expression?
}
@@ -116,3 +221,19 @@ private fun convertSystemOutMethodCall(
private fun CodeConverter.convertToRegex(expression: PsiExpression?): Expression
= MethodCallExpression.build(convertExpression(expression), "toRegex", emptyList(), emptyList(), false).assignNoPrototype()
private fun addIgnoreCaseArgument(
qualifier: PsiExpression?,
methodName: String,
arguments: Array<PsiExpression>,
typeArgumentsConverted: List<Type>,
codeConverter: CodeConverter,
ignoreCaseArgument: PsiExpression? = null
): Expression {
val ignoreCaseExpression = ignoreCaseArgument?.let { codeConverter.convertExpression(it) } ?: LiteralExpression("true").assignNoPrototype()
val ignoreCaseArgumentExpression = AssignmentExpression(Identifier("ignoreCase").assignNoPrototype(), ignoreCaseExpression, "=").assignNoPrototype()
return MethodCallExpression.build(codeConverter.convertExpression(qualifier), methodName,
codeConverter.convertExpressions(arguments) + ignoreCaseArgumentExpression,
typeArgumentsConverted, false)
}
@@ -33,6 +33,8 @@ class Block(val statements: List<Statement>, val lBrace: LBrace, val rBrace: RBr
companion object {
val Empty = Block(listOf(), LBrace(), RBrace())
fun of(statement: Statement) = of(listOf(statement))
fun of(statements: List<Statement>) = Block(statements, LBrace().assignNoPrototype(), RBrace().assignNoPrototype(), notEmpty = true)
}
}
@@ -0,0 +1,117 @@
import java.nio.charset.Charset;
import java.util.*;
class A {
void constructors() throws Exception {
new String();
// TODO: new String("original");
new String(new char[] {'a', 'b', 'c'});
new String(new char[] {'b', 'd'}, 1, 1);
new String(new int[] { 32, 65, 127 }, 0, 3);
byte[] bytes = new byte[] { 32, 65, 100, 81 };
Charset charset = Charset.forName("utf-8");
new String(bytes);
new String(bytes, charset);
new String(bytes, 0, 2);
new String(bytes, "utf-8");
new String(bytes, 0, 2, "utf-8");
new String(bytes, 0, 2, charset);
new String(new StringBuilder("content"));
new String(new StringBuffer("content"));
}
void normalMethods() {
String s = "test string";
s.length();
s.isEmpty();
s.charAt(1);
s.codePointAt(2);
s.codePointBefore(2);
s.codePointCount(0, s.length());
s.offsetByCodePoints(0, 4);
s.compareTo("test 2");
s.concat(" another");
s.contains("seq");
s.contentEquals(new StringBuilder(s));
s.contentEquals(new StringBuffer(s));
s.endsWith("ng");
s.startsWith("te");
s.startsWith("st", 2);
s.indexOf("st");
s.indexOf("st", 5);
s.substring(1);
s.substring(0, 4);
s.subSequence(0, 4);
s.replace('e', 'i');
s.replace("est", "oast");
s.intern();
s.toLowerCase();
s.toLowerCase(Locale.FRENCH);
s.toUpperCase();
s.toUpperCase(Locale.FRENCH);
s.toString();
s.toCharArray();
char[] chars = new char[10];
s.getChars(1, 11, chars, 0);
}
void specialMethods() throws Exception {
String s = "test string";
s.equals("test");
s.equalsIgnoreCase("tesT");
s.compareToIgnoreCase("Test");
s.regionMatches(true, 0, "TE", 0, 2);
s.regionMatches(0, "st", 1, 2);
s.matches("\\w+");
s.replaceAll("\\w+", "---");
s.replaceFirst("([s-t])", "A$1");
useSplit(s.split("\\s+"));
useSplit(s.split("\\s+", 0));
useSplit(s.split("\\s+", -1));
useSplit(s.split("\\s+", 2));
int limit = 5;
useSplit(s.split("\\s+", limit));
s.trim();
s.getBytes();
s.getBytes(Charset.forName("utf-8"));
s.getBytes("utf-8");
}
void staticMethods() {
String.valueOf(1);
String.valueOf(1L);
String.valueOf('a');
String.valueOf(true);
String.valueOf(1.11F);
String.valueOf(3.14);
String.valueOf(new Object());
String.format(Locale.FRENCH, "Je ne mange pas %d jours", 6);
String.format("Operation completed with %s", "success");
char[] chars = {'a', 'b', 'c'};
String.valueOf(chars);
String.valueOf(chars, 1, 2);
String.copyValueOf(chars);
String.copyValueOf(chars, 1, 2);
Comparator<String> order = String.CASE_INSENSITIVE_ORDER;
}
void unsupportedMethods() {
String s = "test string";
/* TODO:
s.indexOf(32);
s.indexOf(32, 2);
s.lastIndexOf(32);
s.lastIndexOf(32, 2);
*/
}
void useSplit(String[] result) {}
}
@@ -0,0 +1,127 @@
// ERROR: Overload resolution ambiguity: public fun kotlin.String.split(regex: [ERROR : java.util.regex.Pattern], limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin public fun kotlin.String.split(pattern: kotlin.text.Regex, limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin
// ERROR: Unresolved reference: it
// ERROR: Overload resolution ambiguity: public fun kotlin.String.split(regex: [ERROR : java.util.regex.Pattern], limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin public fun kotlin.String.split(pattern: kotlin.text.Regex, limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin
// ERROR: Unresolved reference: it
// ERROR: Overload resolution ambiguity: public fun kotlin.String.split(regex: [ERROR : java.util.regex.Pattern], limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin public fun kotlin.String.split(pattern: kotlin.text.Regex, limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin
// ERROR: Overload resolution ambiguity: public fun kotlin.String.split(regex: [ERROR : java.util.regex.Pattern], limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin public fun kotlin.String.split(pattern: kotlin.text.Regex, limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin
// ERROR: Overload resolution ambiguity: public fun kotlin.String.split(regex: [ERROR : java.util.regex.Pattern], limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin public fun kotlin.String.split(pattern: kotlin.text.Regex, limit: kotlin.Int = ...): kotlin.List<kotlin.String> defined in kotlin
import java.nio.charset.Charset
import java.util.*
internal class A {
@Throws(Exception::class)
fun constructors() {
String()
// TODO: new String("original");
String(charArrayOf('a', 'b', 'c'))
String(charArrayOf('b', 'd'), 1, 1)
String(intArrayOf(32, 65, 127), 0, 3)
val bytes = byteArrayOf(32, 65, 100, 81)
val charset = Charset.forName("utf-8")
String(bytes)
String(bytes, charset)
String(bytes, 0, 2)
String(bytes, "utf-8")
String(bytes, 0, 2, "utf-8")
String(bytes, 0, 2, charset)
String(StringBuilder("content"))
String(StringBuffer("content"))
}
fun normalMethods() {
val s = "test string"
s.length()
s.isEmpty()
s.charAt(1)
s.codePointAt(2)
s.codePointBefore(2)
s.codePointCount(0, s.length())
s.offsetByCodePoints(0, 4)
s.compareTo("test 2")
s.concat(" another")
s.contains("seq")
s.contentEquals(StringBuilder(s))
s.contentEquals(StringBuffer(s))
s.endsWith("ng")
s.startsWith("te")
s.startsWith("st", 2)
s.indexOf("st")
s.indexOf("st", 5)
s.substring(1)
s.substring(0, 4)
s.subSequence(0, 4)
s.replace('e', 'i')
s.replace("est", "oast")
s.intern()
s.toLowerCase()
s.toLowerCase(Locale.FRENCH)
s.toUpperCase()
s.toUpperCase(Locale.FRENCH)
s.toString()
s.toCharArray()
val chars = CharArray(10)
s.getChars(1, 11, chars, 0)
}
@Throws(Exception::class)
fun specialMethods() {
val s = "test string"
s == "test"
s.equals("tesT", ignoreCase = true)
s.compareTo("Test", ignoreCase = true)
s.regionMatches(0, "TE", 0, 2, ignoreCase = true)
s.regionMatches(0, "st", 1, 2)
s.matches("\\w+".toRegex())
s.replace("\\w+".toRegex(), "---")
s.replaceFirst("([s-t])".toRegex(), "A$1")
useSplit(s.split("\\s+".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray())
useSplit(s.split("\\s+".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray())
useSplit(s.split("\\s+".toRegex()).toTypedArray())
useSplit(s.split("\\s+".toRegex(), 2).toTypedArray())
val limit = 5
useSplit(s.split("\\s+".toRegex(), limit.coerceAtLeast(0)).toTypedArray())
s.trim { it <= ' ' }
s.toByteArray()
s.toByteArray(Charset.forName("utf-8"))
s.toByteArray("utf-8")
}
fun staticMethods() {
1.toString()
1L.toString()
'a'.toString()
true.toString()
1.11f.toString()
3.14.toString()
Object().toString()
"Je ne mange pas %d jours".format(Locale.FRENCH, 6)
"Operation completed with %s".format("success")
val chars = charArrayOf('a', 'b', 'c')
String(chars)
String(chars, 1, 2)
String(chars)
String(chars, 1, 2)
val order = String.CASE_INSENSITIVE_ORDER
}
fun unsupportedMethods() {
val s = "test string"
/* TODO:
s.indexOf(32);
s.indexOf(32, 2);
s.lastIndexOf(32);
s.lastIndexOf(32, 2);
*/
}
fun useSplit(result: Array<String>) {
}
}
@@ -3148,6 +3148,12 @@ public class JavaToKotlinConverterForWebDemoTestGenerated extends AbstractJavaTo
doTest(fileName);
}
@TestMetadata("stringMethods.java")
public void testStringMethods() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/methodCallExpression/stringMethods.java");
doTest(fileName);
}
@TestMetadata("systemOut.java")
public void testSystemOut() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/methodCallExpression/systemOut.java");
@@ -3148,6 +3148,12 @@ public class JavaToKotlinConverterSingleFileTestGenerated extends AbstractJavaTo
doTest(fileName);
}
@TestMetadata("stringMethods.java")
public void testStringMethods() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/methodCallExpression/stringMethods.java");
doTest(fileName);
}
@TestMetadata("systemOut.java")
public void testSystemOut() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/methodCallExpression/systemOut.java");