From 6f5d9777281894a9da963e5cd1e08a5db58761f4 Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Thu, 15 Oct 2015 18:57:08 +0300 Subject: [PATCH] Inspections: Add inspection on equals()/hashCode() --- .../EqualsOrHashCode.html | 6 ++ idea/src/META-INF/plugin.xml | 6 ++ .../KotlinGenerateEqualsAndHashcodeAction.kt | 46 ++++----- .../inspections/EqualsOrHashCodeInspection.kt | 99 +++++++++++++++++++ .../inspectionData/expected.xml | 42 ++++++++ .../inspectionData/inspections.test | 1 + .../inspections/equalsAndHashCode/test.kt | 39 ++++++++ .../JetInspectionTestGenerated.java | 6 ++ 8 files changed, 222 insertions(+), 23 deletions(-) create mode 100644 idea/resources/inspectionDescriptions/EqualsOrHashCode.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/EqualsOrHashCodeInspection.kt create mode 100644 idea/testData/inspections/equalsAndHashCode/inspectionData/expected.xml create mode 100644 idea/testData/inspections/equalsAndHashCode/inspectionData/inspections.test create mode 100644 idea/testData/inspections/equalsAndHashCode/test.kt diff --git a/idea/resources/inspectionDescriptions/EqualsOrHashCode.html b/idea/resources/inspectionDescriptions/EqualsOrHashCode.html new file mode 100644 index 00000000000..c939447b7ca --- /dev/null +++ b/idea/resources/inspectionDescriptions/EqualsOrHashCode.html @@ -0,0 +1,6 @@ + + +This inspection reports classes that override equals() but do not override hashCode(), or vice versa, which could potentially lead to problems when class is added to a Collection. +It also reports object declarations which override either equals() or hashCode() + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 173ab8c93c9..174a5e4246b 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1271,6 +1271,12 @@ cleanupTool="true" level="WARNING"/> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/generate/KotlinGenerateEqualsAndHashcodeAction.kt b/idea/src/org/jetbrains/kotlin/idea/actions/generate/KotlinGenerateEqualsAndHashcodeAction.kt index 239aabbf7f1..30cd2c5819c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/actions/generate/KotlinGenerateEqualsAndHashcodeAction.kt +++ b/idea/src/org/jetbrains/kotlin/idea/actions/generate/KotlinGenerateEqualsAndHashcodeAction.kt @@ -50,6 +50,29 @@ import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.addToStdlib.lastIsInstanceOrNull import java.util.* +private tailrec fun ClassDescriptor.findDeclaredFunction ( + name: String, + checkSuperClasses: Boolean, + filter: (FunctionDescriptor) -> Boolean +): FunctionDescriptor? { + unsubstitutedMemberScope + .getFunctions(Name.identifier(name), NoLookupLocation.FROM_IDE) + .firstOrNull { it.containingDeclaration == this && it.kind == CallableMemberDescriptor.Kind.DECLARATION && filter(it) } + ?.let { return it } + + return if (checkSuperClasses) getSuperClassOrAny().findDeclaredFunction(name, checkSuperClasses, filter) else null +} + +fun ClassDescriptor.findDeclaredEquals(checkSupers: Boolean): FunctionDescriptor? { + return findDeclaredFunction("equals", checkSupers) { + it.valueParameters.singleOrNull()?.type == it.builtIns.nullableAnyType && it.typeParameters.isEmpty() + } +} + +fun ClassDescriptor.findDeclaredHashCode(checkSupers: Boolean): FunctionDescriptor? { + return findDeclaredFunction("hashCode", checkSupers) { it.valueParameters.isEmpty() && it.typeParameters.isEmpty() } +} + class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase() { companion object { private val LOG = Logger.getInstance(KotlinGenerateEqualsAndHashcodeAction::class.java) @@ -75,29 +98,6 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase Boolean - ): FunctionDescriptor? { - unsubstitutedMemberScope - .getFunctions(Name.identifier(name), NoLookupLocation.FROM_IDE) - .firstOrNull { it.containingDeclaration == this && it.kind == CallableMemberDescriptor.Kind.DECLARATION && filter(it) } - ?.let { return it } - - return if (checkSuperClasses) getSuperClassOrAny().findDeclaredFunction(name, checkSuperClasses, filter) else null - } - - private fun ClassDescriptor.findDeclaredEquals(checkSupers: Boolean): FunctionDescriptor? { - return findDeclaredFunction("equals", checkSupers) { - it.valueParameters.singleOrNull()?.type == it.builtIns.nullableAnyType && it.typeParameters.isEmpty() - } - } - - private fun ClassDescriptor.findDeclaredHashCode(checkSupers: Boolean): FunctionDescriptor? { - return findDeclaredFunction("hashCode", checkSupers) { it.valueParameters.isEmpty() && it.typeParameters.isEmpty() } - } - private fun JetClassOrObject.getPropertiesToUse(): List { return ArrayList().apply { getPrimaryConstructorParameters().filterTo(this) { it.hasValOrVar() } diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/EqualsOrHashCodeInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/EqualsOrHashCodeInspection.kt new file mode 100644 index 00000000000..b59e003f175 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/EqualsOrHashCodeInspection.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2015 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.InspectionsBundle +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsAndHashcodeAction +import org.jetbrains.kotlin.idea.actions.generate.findDeclaredEquals +import org.jetbrains.kotlin.idea.actions.generate.findDeclaredHashCode +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.psi.JetClass +import org.jetbrains.kotlin.psi.JetClassOrObject +import org.jetbrains.kotlin.psi.JetObjectDeclaration +import org.jetbrains.kotlin.psi.JetVisitorVoid +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.resolve.source.getPsi + +object DeleteEqualsAndHashCodeFix : LocalQuickFix { + override fun getName() = "Delete equals()/hashCode()" + + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val objectDeclaration = descriptor.psiElement.getStrictParentOfType() ?: return + val classDescriptor = objectDeclaration.resolveToDescriptorIfAny() as? ClassDescriptor ?: return + classDescriptor.findDeclaredEquals(false)?.source?.getPsi()?.delete() + classDescriptor.findDeclaredHashCode(false)?.source?.getPsi()?.delete() + } +} + +sealed class GenerateEqualsOrHashCodeFix : LocalQuickFix { + object Equals : GenerateEqualsOrHashCodeFix() { + override fun getName() = "Generate 'equals()'" + } + + object HashCode : GenerateEqualsOrHashCodeFix() { + override fun getName() = "Generate 'hashCode()'" + } + + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + KotlinGenerateEqualsAndHashcodeAction().doInvoke(project, null, descriptor.psiElement.parent as JetClass) + } +} + +class EqualsOrHashCodeInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object: JetVisitorVoid() { + override fun visitClassOrObject(classOrObject: JetClassOrObject) { + val nameIdentifier = classOrObject.nameIdentifier ?: return + val classDescriptor = classOrObject.resolveToDescriptorIfAny() as? ClassDescriptor ?: return + val hasEquals = classDescriptor.findDeclaredEquals(false) != null + val hasHashCode = classDescriptor.findDeclaredHashCode(false) != null + if (!hasEquals && !hasHashCode) return + + when (classDescriptor.kind) { + ClassKind.OBJECT -> { + holder.registerProblem(nameIdentifier, "equals()/hashCode() in object declaration", DeleteEqualsAndHashCodeFix) + } + ClassKind.CLASS -> { + if (hasEquals && hasHashCode) return + val description = InspectionsBundle.message( + "inspection.equals.hashcode.only.one.defined.problem.descriptor", + if (hasEquals) "equals()" else "hashCode()", + if (hasEquals) "hashCode()" else "equals()" + ) + holder.registerProblem( + nameIdentifier, + description, + if (hasEquals) GenerateEqualsOrHashCodeFix.HashCode else GenerateEqualsOrHashCodeFix.Equals + ) + } + else -> return + } + } + } + } +} \ No newline at end of file diff --git a/idea/testData/inspections/equalsAndHashCode/inspectionData/expected.xml b/idea/testData/inspections/equalsAndHashCode/inspectionData/expected.xml new file mode 100644 index 00000000000..5e00c114ac0 --- /dev/null +++ b/idea/testData/inspections/equalsAndHashCode/inspectionData/expected.xml @@ -0,0 +1,42 @@ + + + test.kt + 1 + light_idea_test_case + + equals() and hashCode() not paired + Class has <code>equals()</code> defined but does not define <code>hashCode()</code> + + + test.kt + 5 + light_idea_test_case + + equals() and hashCode() not paired + Class has <code>hashCode()</code> defined but does not define <code>equals()</code> + + + test.kt + 14 + light_idea_test_case + + equals() and hashCode() not paired + equals()/hashCode() in object declaration + + + test.kt + 18 + light_idea_test_case + + equals() and hashCode() not paired + equals()/hashCode() in object declaration + + + test.kt + 22 + light_idea_test_case + + equals() and hashCode() not paired + equals()/hashCode() in object declaration + + diff --git a/idea/testData/inspections/equalsAndHashCode/inspectionData/inspections.test b/idea/testData/inspections/equalsAndHashCode/inspectionData/inspections.test new file mode 100644 index 00000000000..b3f3343d70a --- /dev/null +++ b/idea/testData/inspections/equalsAndHashCode/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.EqualsOrHashCodeInspection diff --git a/idea/testData/inspections/equalsAndHashCode/test.kt b/idea/testData/inspections/equalsAndHashCode/test.kt new file mode 100644 index 00000000000..2bd043e6030 --- /dev/null +++ b/idea/testData/inspections/equalsAndHashCode/test.kt @@ -0,0 +1,39 @@ +class C1 { + override fun equals(other: Any?) = true +} + +class C2 { + override fun hashCode() = 0 +} + +class C3 { + override fun equals(other: Any?) = true + override fun hashCode() = 0 +} + +object O1 { + override fun equals(other: Any?) = true +} + +object O2 { + override fun hashCode() = 0 +} + +object O3 { + override fun equals(other: Any?) = true + override fun hashCode() = 0 +} + +class C4 { + override fun equals(other: ะก4) = true +} + +interface I { + override fun equals(other: Any?) = true + override fun hashCode() = 0 +} + +enum E { + override fun equals(other: Any?) = true + override fun hashCode() = 0 +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java index 6ffca985581..f0891b1e9c6 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java @@ -100,6 +100,12 @@ public class JetInspectionTestGenerated extends AbstractJetInspectionTest { doTest(fileName); } + @TestMetadata("equalsAndHashCode/inspectionData/inspections.test") + public void testEqualsAndHashCode_inspectionData_Inspections_test() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/inspections/equalsAndHashCode/inspectionData/inspections.test"); + doTest(fileName); + } + @TestMetadata("redundantSamConstructor/inspectionData/inspections.test") public void testRedundantSamConstructor_inspectionData_Inspections_test() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/inspections/redundantSamConstructor/inspectionData/inspections.test");