From b8fcdea673089039e19ea0ef5c70f24635a66387 Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Thu, 9 Nov 2017 17:24:30 +0300 Subject: [PATCH] Add inspection for equals which is called recursively within itself So #KT-13702 Fixed --- .../RecursiveEqualsCall.html | 20 +++++ idea/src/META-INF/plugin.xml | 9 +++ .../RecursiveEqualsCallInspection.kt | 75 +++++++++++++++++++ .../recursiveEqualsCall/.inspection | 1 + .../recursiveEqualsCall/recursive.kt | 6 ++ .../recursiveEqualsCall/recursive.kt.after | 6 ++ .../LocalInspectionTestGenerated.java | 15 ++++ 7 files changed, 132 insertions(+) create mode 100644 idea/resources/inspectionDescriptions/RecursiveEqualsCall.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/RecursiveEqualsCallInspection.kt create mode 100644 idea/testData/inspectionsLocal/recursiveEqualsCall/.inspection create mode 100644 idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt create mode 100644 idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt.after diff --git a/idea/resources/inspectionDescriptions/RecursiveEqualsCall.html b/idea/resources/inspectionDescriptions/RecursiveEqualsCall.html new file mode 100644 index 00000000000..a2660c11363 --- /dev/null +++ b/idea/resources/inspectionDescriptions/RecursiveEqualsCall.html @@ -0,0 +1,20 @@ + + +Reports recursive equals calls. + +

+For example: + +

+class X {
+    override fun equals(other: Any?): Boolean {
+        if (this == other) return true // recursive equals call
+        return false
+    }
+}
+
+ +

+ + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 850cf0d9e34..c84efc9d26b 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -2672,6 +2672,15 @@ /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/RecursiveEqualsCallInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/RecursiveEqualsCallInspection.kt new file mode 100644 index 00000000000..fefcbc5ab77 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/RecursiveEqualsCallInspection.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2010-2017 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.inspections + +import com.intellij.codeInspection.* +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClass +import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.typeUtil.isBoolean +import org.jetbrains.kotlin.types.typeUtil.isNullableAny + +class RecursiveEqualsCallInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { + return object : KtVisitorVoid() { + + override fun visitBinaryExpression(expr: KtBinaryExpression) { + super.visitBinaryExpression(expr) + + if (expr.operationToken != KtTokens.EQEQ) return + + val classDescriptor = expr.containingClass()?.descriptor ?: return + val context = expr.analyze(BodyResolveMode.PARTIAL) + if (classDescriptor != expr.getResolvedCall(context)?.dispatchReceiver?.type?.constructor?.declarationDescriptor) return + + val functionDescriptor = expr.getNonStrictParentOfType()?.descriptor as? FunctionDescriptor ?: return + if (functionDescriptor.name.asString() != "equals" || + !DescriptorUtils.isOverride(functionDescriptor) || + functionDescriptor.valueParameters.singleOrNull()?.type?.isNullableAny() == false || + functionDescriptor.returnType?.isBoolean() == false) return + + holder.registerProblem(expr, + "Recursive equals call", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + ReplaceWithReferentialEqualityFix()) + } + } + } +} + +private class ReplaceWithReferentialEqualityFix : LocalQuickFix { + override fun getName() = "Replace with '==='" + + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val expr = descriptor.psiElement as? KtBinaryExpression ?: return + val left = expr.left ?: return + val right = expr.right ?: return + expr.replace(KtPsiFactory(project).createExpressionByPattern("$0 $1 $2", left, "===", right)) + } +} + diff --git a/idea/testData/inspectionsLocal/recursiveEqualsCall/.inspection b/idea/testData/inspectionsLocal/recursiveEqualsCall/.inspection new file mode 100644 index 00000000000..5e3a0a6f231 --- /dev/null +++ b/idea/testData/inspectionsLocal/recursiveEqualsCall/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.RecursiveEqualsCallInspection \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt b/idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt new file mode 100644 index 00000000000..ba68f309e6b --- /dev/null +++ b/idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt @@ -0,0 +1,6 @@ +class Test { + override fun equals(other: Any?): Boolean { + if (this == other) return true + return false + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt.after b/idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt.after new file mode 100644 index 00000000000..03d3e0d76a4 --- /dev/null +++ b/idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt.after @@ -0,0 +1,6 @@ +class Test { + override fun equals(other: Any?): Boolean { + if (this === other) return true + return false + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index 71e27e824b9..2742b1d97fc 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -1500,6 +1500,21 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { } } + @TestMetadata("idea/testData/inspectionsLocal/recursiveEqualsCall") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class RecursiveEqualsCall extends AbstractLocalInspectionTest { + public void testAllFilesPresentInRecursiveEqualsCall() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/recursiveEqualsCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("recursive.kt") + public void testRecursive() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/recursiveEqualsCall/recursive.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/inspectionsLocal/redundantExplicitType") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)