Implement quickfix for wrong primitive literal (#885)

* Implement quickfix for wrong primitive literal
Fixes: KT-12251

* fix style issue
This commit is contained in:
Kirill Rakhman
2016-07-05 13:34:14 +02:00
committed by Dmitry Jemerov
parent 3b290ce3dd
commit b50176fa2e
27 changed files with 271 additions and 8 deletions
@@ -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,6 +1,6 @@
// "Convert expression to 'Int'" "true"
fun foo() {
bar(1L<caret>)
bar("1".toLong()<caret>)
}
fun bar(l: Int) {
@@ -1,6 +1,6 @@
// "Convert expression to 'Int'" "true"
fun foo() {
bar(1L.toInt()<caret>)
bar("1".toLong().toInt()<caret>)
}
fun bar(l: Int) {
@@ -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>
@@ -0,0 +1,2 @@
// "Change to '1.2'" "true"
val a : Double = 1.2F<caret>
@@ -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")