Implement quickfix for wrong primitive literal (#885)
* Implement quickfix for wrong primitive literal Fixes: KT-12251 * fix style issue
This commit is contained in:
committed by
Dmitry Jemerov
parent
3b290ce3dd
commit
b50176fa2e
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.jetbrains.kotlin.idea.quickfix
|
||||
|
||||
import com.intellij.codeInsight.intention.IntentionAction
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiFile
|
||||
@@ -31,7 +32,11 @@ import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.types.typeUtil.isPrimitiveNumberType
|
||||
|
||||
class NumberConversionFix(element: KtExpression, type: KotlinType) : KotlinQuickFixAction<KtExpression>(element) {
|
||||
class NumberConversionFix(
|
||||
element: KtExpression,
|
||||
type: KotlinType,
|
||||
private val disableIfAvailable: IntentionAction? = null
|
||||
) : KotlinQuickFixAction<KtExpression>(element) {
|
||||
private val isConversionAvailable: Boolean = run {
|
||||
val expressionType = element.analyze(BodyResolveMode.PARTIAL).getType(element)
|
||||
expressionType != null && expressionType != type && expressionType.isPrimitiveNumberType() && type.isPrimitiveNumberType()
|
||||
@@ -39,7 +44,9 @@ class NumberConversionFix(element: KtExpression, type: KotlinType) : KotlinQuick
|
||||
private val typePresentation = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(type)
|
||||
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile)
|
||||
= isConversionAvailable && super.isAvailable(project, editor, file)
|
||||
= disableIfAvailable?.isAvailable(project, editor, file) != true
|
||||
&& isConversionAvailable
|
||||
&& super.isAvailable(project, editor, file)
|
||||
|
||||
override fun getFamilyName() = "Insert number conversion"
|
||||
override fun getText() = "Convert expression to '$typePresentation'"
|
||||
|
||||
@@ -83,7 +83,12 @@ class QuickFixFactoryForTypeMismatchError : KotlinIntentionActionsFactory() {
|
||||
}
|
||||
|
||||
if (expressionType.isPrimitiveNumberType() && expectedType.isPrimitiveNumberType()) {
|
||||
actions.add(NumberConversionFix(diagnosticElement, expectedType))
|
||||
var wrongPrimitiveLiteralFix: WrongPrimitiveLiteralFix? = null
|
||||
if (diagnosticElement is KtConstantExpression && !KotlinBuiltIns.isChar(expectedType)) {
|
||||
wrongPrimitiveLiteralFix = WrongPrimitiveLiteralFix(diagnosticElement, expectedType)
|
||||
actions.add(wrongPrimitiveLiteralFix)
|
||||
}
|
||||
actions.add(NumberConversionFix(diagnosticElement, expectedType, wrongPrimitiveLiteralFix))
|
||||
}
|
||||
|
||||
if (KotlinBuiltIns.isCharSequenceOrNullableCharSequence(expectedType)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2010-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.quickfix
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiFile
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.codegen.ExpressionCodegen
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.core.replaced
|
||||
import org.jetbrains.kotlin.psi.KtConstantExpression
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.KtPsiFactory
|
||||
import org.jetbrains.kotlin.psi.psiUtil.endOffset
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
|
||||
private val valueRanges = mapOf(
|
||||
KotlinBuiltIns.FQ_NAMES._byte to Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong(),
|
||||
KotlinBuiltIns.FQ_NAMES._short to Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong(),
|
||||
KotlinBuiltIns.FQ_NAMES._int to Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong(),
|
||||
KotlinBuiltIns.FQ_NAMES._long to Long.MIN_VALUE..Long.MAX_VALUE
|
||||
)
|
||||
|
||||
class WrongPrimitiveLiteralFix(element: KtConstantExpression, type: KotlinType) : KotlinQuickFixAction<KtExpression>(element) {
|
||||
|
||||
private val typeName = DescriptorUtils.getFqName(type.constructor.declarationDescriptor!!)
|
||||
private val expectedTypeIsFloat = KotlinBuiltIns.isFloat(type)
|
||||
private val expectedTypeIsDouble = KotlinBuiltIns.isDouble(type)
|
||||
private val constValue
|
||||
= ExpressionCodegen.getPrimitiveOrStringCompileTimeConstant(element, element.analyze(BodyResolveMode.PARTIAL))?.value as? Number
|
||||
|
||||
private val fixedExpression = buildString {
|
||||
if (expectedTypeIsFloat || expectedTypeIsDouble) {
|
||||
append(constValue)
|
||||
if (expectedTypeIsFloat) {
|
||||
append('F')
|
||||
}
|
||||
else if ('.' !in this) {
|
||||
append(".0")
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (constValue is Float || constValue is Double) {
|
||||
append(constValue.toLong())
|
||||
}
|
||||
else {
|
||||
append(element.text.trimEnd('l', 'L'))
|
||||
}
|
||||
|
||||
if (KotlinBuiltIns.isLong(type)) {
|
||||
append('L')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean {
|
||||
if (!super.isAvailable(project, editor, file)) return false
|
||||
if (constValue == null) return false
|
||||
if (expectedTypeIsFloat || expectedTypeIsDouble) return true
|
||||
|
||||
if (constValue is Float || constValue is Double) {
|
||||
val value = constValue.toDouble()
|
||||
if (value != Math.floor(value)) return false
|
||||
if (value !in Long.MIN_VALUE.toDouble()..Long.MAX_VALUE.toDouble()) return false
|
||||
}
|
||||
|
||||
return constValue.toLong() in valueRanges[typeName] ?: return false
|
||||
}
|
||||
|
||||
override fun getFamilyName() = "Change to correct primitive type"
|
||||
override fun getText() = "Change to '$fixedExpression'"
|
||||
|
||||
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
|
||||
val expressionToInsert = KtPsiFactory(file).createExpression(fixedExpression)
|
||||
val newExpression = element.replaced(expressionToInsert)
|
||||
editor?.caretModel?.moveToOffset(newExpression.endOffset)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// "Convert expression to 'Int'" "true"
|
||||
fun foo() {
|
||||
bar(1L<caret>)
|
||||
bar("1".toLong()<caret>)
|
||||
}
|
||||
|
||||
fun bar(l: Int) {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// "Convert expression to 'Int'" "true"
|
||||
fun foo() {
|
||||
bar(1L.toInt()<caret>)
|
||||
bar("1".toLong().toInt()<caret>)
|
||||
}
|
||||
|
||||
fun bar(l: Int) {
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Change to '1'" "false"
|
||||
// ACTION: Add 'const' modifier
|
||||
// ACTION: Change 'a' type to 'Double'
|
||||
// ACTION: Convert expression to 'Int'
|
||||
// ACTION: Convert property initializer to getter
|
||||
// ERROR: The floating-point literal does not conform to the expected type Int
|
||||
|
||||
val a : Int = 1.12<caret>
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Change to '10000000000000000000L'" "false"
|
||||
// ACTION: Add 'const' modifier
|
||||
// ACTION: Change 'a' type to 'Double'
|
||||
// ACTION: Convert expression to 'Long'
|
||||
// ACTION: Convert property initializer to getter
|
||||
// ERROR: The floating-point literal does not conform to the expected type Long
|
||||
|
||||
val a : Long = 10000000000000000000.0<caret>
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Change to '65000'" "false"
|
||||
// ACTION: Add 'const' modifier
|
||||
// ACTION: Change 'a' type to 'Double'
|
||||
// ACTION: Convert expression to 'Short'
|
||||
// ACTION: Convert property initializer to getter
|
||||
// ERROR: The floating-point literal does not conform to the expected type Short
|
||||
|
||||
val a : Short = 65000.0<caret>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// "Change to '1.2'" "true"
|
||||
val a : Double = 1.2F<caret>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// "Change to '1.2'" "true"
|
||||
val a : Double = 1.2<caret>
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Change to '1'" "true"
|
||||
// ACTION: Add 'const' modifier
|
||||
// ACTION: Change 'a' type to 'Double'
|
||||
// ACTION: Convert expression to 'Int'
|
||||
// ACTION: Convert property initializer to getter
|
||||
|
||||
val a : Int = 1.0F<caret>
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Change to '1'" "true"
|
||||
// ACTION: Add 'const' modifier
|
||||
// ACTION: Change 'a' type to 'Double'
|
||||
// ACTION: Convert expression to 'Int'
|
||||
// ACTION: Convert property initializer to getter
|
||||
|
||||
val a : Int = 1<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1'" "true"
|
||||
val a : Int = 1F<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1'" "true"
|
||||
val a : Int = 1<caret>
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Change to '1L'" "true"
|
||||
val a = foo(1.0<caret>)
|
||||
fun foo(l: Long) = l
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Change to '1L'" "true"
|
||||
val a = foo(1L<caret>)
|
||||
fun foo(l: Long) = l
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Change to '15F'" "true"
|
||||
|
||||
val a : Float = 0xF<caret>
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Change to '15F'" "true"
|
||||
|
||||
val a : Float = 15F<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1F'" "true"
|
||||
val a : Float = 1<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1F'" "true"
|
||||
val a : Float = 1F<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1.0'" "true"
|
||||
val a : Double = 1L<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1.0'" "true"
|
||||
val a : Double = 1.0<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1'" "true"
|
||||
val a : Int = 1L<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '1'" "true"
|
||||
val a : Int = 1<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '0b10'" "true"
|
||||
val a : Int = 0b10L<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
// "Change to '0b10'" "true"
|
||||
val a : Int = 0b10<caret>
|
||||
@@ -8307,9 +8307,9 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("convertLiteral.kt")
|
||||
public void testConvertLiteral() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/numberConversion/convertLiteral.kt");
|
||||
@TestMetadata("convertExpression.kt")
|
||||
public void testConvertExpression() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/numberConversion/convertExpression.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
}
|
||||
@@ -8469,6 +8469,87 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class WrongPrimitive extends AbstractQuickFixTest {
|
||||
public void testAllFilesPresentInWrongPrimitive() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/typeMismatch/wrongPrimitive"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
|
||||
}
|
||||
|
||||
@TestMetadata("doubleToIntDecimalPlaces.kt")
|
||||
public void testDoubleToIntDecimalPlaces() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/doubleToIntDecimalPlaces.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("doubleToLongNotInRange.kt")
|
||||
public void testDoubleToLongNotInRange() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/doubleToLongNotInRange.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("doubleToShortNotInRange.kt")
|
||||
public void testDoubleToShortNotInRange() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/doubleToShortNotInRange.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("floatToDoubleWithDecimal.kt")
|
||||
public void testFloatToDoubleWithDecimal() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/floatToDoubleWithDecimal.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("floatToInt.kt")
|
||||
public void testFloatToInt() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/floatToInt.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("floatToInt2.kt")
|
||||
public void testFloatToInt2() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/floatToInt2.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("floatToLong.kt")
|
||||
public void testFloatToLong() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/floatToLong.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("hexToFloat.kt")
|
||||
public void testHexToFloat() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/hexToFloat.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("intToFloat.kt")
|
||||
public void testIntToFloat() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/intToFloat.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("longToDouble.kt")
|
||||
public void testLongToDouble() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/longToDouble.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("longToInt.kt")
|
||||
public void testLongToInt() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/longToInt.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("longToIntBinary.kt")
|
||||
public void testLongToIntBinary() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/wrongPrimitive/longToIntBinary.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/typeParameters")
|
||||
|
||||
Reference in New Issue
Block a user