diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index 62efa1033c5..7f0a06c1b20 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -2368,6 +2368,15 @@ language="kotlin" /> + + + +This inspection reports an equality check with Double.NaN which can be replaced with a.isNaN(). + +For every floating point value a, a == NaN will always result in false. + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/ConvertNaNEqualityInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/ConvertNaNEqualityInspection.kt new file mode 100644 index 00000000000..7c5210dee22 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/ConvertNaNEqualityInspection.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.inspections + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe + +class ConvertNaNEqualityInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return binaryExpressionVisitor { expression -> + if (expression.left.isNaNExpression() || expression.right.isNaNExpression()) { + val inverted = when (expression.operationToken) { + KtTokens.EXCLEQ -> true + KtTokens.EQEQ -> false + else -> return@binaryExpressionVisitor + } + holder.registerProblem( + expression, + "Equality check with NaN should be replaced with 'isNaN()'", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + ConvertNaNEqualityQuickFix(inverted) + ) + } + } + } +} + +private class ConvertNaNEqualityQuickFix(val inverted: Boolean) : LocalQuickFix { + override fun getName() = "Replace with 'isNaN()'" + + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement as? KtBinaryExpression ?: return + + val other = when { + element.left.isNaNExpression() -> element.right ?: return + element.right.isNaNExpression() -> element.left ?: return + else -> return + } + val pattern = if (inverted) "!$0.isNaN()" else "$0.isNaN()" + element.replace(KtPsiFactory(element).createExpressionByPattern(pattern, other)) + } +} + +private val NaNSet = setOf("kotlin.Double.Companion.NaN", "java.lang.Double.NaN", "kotlin.Float.Companion.NaN", "java.lang.Float.NaN") + +private fun KtExpression?.isNaNExpression(): Boolean { + if (this?.text?.endsWith("NaN") != true) return false + val fqName = this.resolveToCall()?.resultingDescriptor?.fqNameUnsafe?.asString() + return NaNSet.contains(fqName) +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/.inspection b/idea/testData/inspectionsLocal/convertNaNEquality/.inspection new file mode 100644 index 00000000000..e925f5610e7 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.ConvertEqualsDoubleNaNInspection diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/float.kt b/idea/testData/inspectionsLocal/convertNaNEquality/float.kt new file mode 100644 index 00000000000..b987ebee194 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/float.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun test() { + val x = 0.5f != Float.NaN +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/float.kt.after b/idea/testData/inspectionsLocal/convertNaNEquality/float.kt.after new file mode 100644 index 00000000000..2b75acb6d32 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/float.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun test() { + val x = !0.5f.isNaN() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/importedProperty.kt b/idea/testData/inspectionsLocal/convertNaNEquality/importedProperty.kt new file mode 100644 index 00000000000..d5f71a54de6 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/importedProperty.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import kotlin.Double.Companion.NaN + +fun test() { + val t = NaN == 5.0 +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/importedProperty.kt.after b/idea/testData/inspectionsLocal/convertNaNEquality/importedProperty.kt.after new file mode 100644 index 00000000000..22add4edc87 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/importedProperty.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import kotlin.Double.Companion.NaN + +fun test() { + val t = 5.0.isNaN() +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/inequality.kt b/idea/testData/inspectionsLocal/convertNaNEquality/inequality.kt new file mode 100644 index 00000000000..7f8bdd2b668 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/inequality.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun main() { + val result = 5.0 + 3.0 != Double.NaN; +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/inequality.kt.after b/idea/testData/inspectionsLocal/convertNaNEquality/inequality.kt.after new file mode 100644 index 00000000000..41484f626fc --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/inequality.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun main() { + val result = !(5.0 + 3.0).isNaN(); +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/javaDouble.kt b/idea/testData/inspectionsLocal/convertNaNEquality/javaDouble.kt new file mode 100644 index 00000000000..6d7fb68c6ef --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/javaDouble.kt @@ -0,0 +1,5 @@ +// RUNTIME_WITH_FULL_JDK + +fun test() { + val t = java.lang.Double.NaN == 5.0 +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/javaDouble.kt.after b/idea/testData/inspectionsLocal/convertNaNEquality/javaDouble.kt.after new file mode 100644 index 00000000000..aee616c96f2 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/javaDouble.kt.after @@ -0,0 +1,5 @@ +// RUNTIME_WITH_FULL_JDK + +fun test() { + val t = 5.0.isNaN() +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/javaFloatNaN.kt b/idea/testData/inspectionsLocal/convertNaNEquality/javaFloatNaN.kt new file mode 100644 index 00000000000..522a5e15845 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/javaFloatNaN.kt @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK + +fun test() { + val a = 0.5f + val x = a == java.lang.Float.NaN +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/javaFloatNaN.kt.after b/idea/testData/inspectionsLocal/convertNaNEquality/javaFloatNaN.kt.after new file mode 100644 index 00000000000..242d89570eb --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/javaFloatNaN.kt.after @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK + +fun test() { + val a = 0.5f + val x = a.isNaN() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/negative.kt b/idea/testData/inspectionsLocal/convertNaNEquality/negative.kt new file mode 100644 index 00000000000..c88cfa4caaf --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/negative.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +// PROBLEM: none + +class A { + val NaN = 0.3 +} + +fun test() { + val a = A() + val t = a.NaN == 0.5 +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/simple.kt b/idea/testData/inspectionsLocal/convertNaNEquality/simple.kt new file mode 100644 index 00000000000..e0016de5d68 --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/simple.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun test() { + val t = 5.0 == Double.NaN +} diff --git a/idea/testData/inspectionsLocal/convertNaNEquality/simple.kt.after b/idea/testData/inspectionsLocal/convertNaNEquality/simple.kt.after new file mode 100644 index 00000000000..0754e6c60ad --- /dev/null +++ b/idea/testData/inspectionsLocal/convertNaNEquality/simple.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun test() { + val t = 5.0.isNaN() +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index 1997ca2405a..d1e734f1c45 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -2137,6 +2137,54 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { } } + @TestMetadata("idea/testData/inspectionsLocal/convertNaNEquality") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ConvertNaNEquality extends AbstractLocalInspectionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInConvertNaNEquality() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/convertNaNEquality"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); + } + + @TestMetadata("float.kt") + public void testFloat() throws Exception { + runTest("idea/testData/inspectionsLocal/convertNaNEquality/float.kt"); + } + + @TestMetadata("importedProperty.kt") + public void testImportedProperty() throws Exception { + runTest("idea/testData/inspectionsLocal/convertNaNEquality/importedProperty.kt"); + } + + @TestMetadata("inequality.kt") + public void testInequality() throws Exception { + runTest("idea/testData/inspectionsLocal/convertNaNEquality/inequality.kt"); + } + + @TestMetadata("javaDouble.kt") + public void testJavaDouble() throws Exception { + runTest("idea/testData/inspectionsLocal/convertNaNEquality/javaDouble.kt"); + } + + @TestMetadata("javaFloatNaN.kt") + public void testJavaFloatNaN() throws Exception { + runTest("idea/testData/inspectionsLocal/convertNaNEquality/javaFloatNaN.kt"); + } + + @TestMetadata("negative.kt") + public void testNegative() throws Exception { + runTest("idea/testData/inspectionsLocal/convertNaNEquality/negative.kt"); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("idea/testData/inspectionsLocal/convertNaNEquality/simple.kt"); + } + } + @TestMetadata("idea/testData/inspectionsLocal/convertPairConstructorToToFunction") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)