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");