Inspections: Add inspection on equals()/hashCode()

This commit is contained in:
Alexey Sedunov
2015-10-15 18:57:08 +03:00
parent 127cb36602
commit 6f5d977728
8 changed files with 222 additions and 23 deletions
@@ -0,0 +1,6 @@
<html>
<body>
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()
</body>
</html>
+6
View File
@@ -1271,6 +1271,12 @@
cleanupTool="true"
level="WARNING"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.EqualsOrHashCodeInspection"
displayName="equals() and hashCode() not paired"
groupName="Kotlin"
enabledByDefault="true"
level="WARNING"/>
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
@@ -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<KotlinGenerateEqualsAndHashcodeAction.Info>() {
companion object {
private val LOG = Logger.getInstance(KotlinGenerateEqualsAndHashcodeAction::class.java)
@@ -75,29 +98,6 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
&& targetClass.getPropertiesToUse().isNotEmpty()
}
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
}
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<JetNamedDeclaration> {
return ArrayList<JetNamedDeclaration>().apply {
getPrimaryConstructorParameters().filterTo(this) { it.hasValOrVar() }
@@ -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<JetObjectDeclaration>() ?: 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) "<code>equals()</code>" else "<code>hashCode()</code>",
if (hasEquals) "<code>hashCode()</code>" else "<code>equals()</code>"
)
holder.registerProblem(
nameIdentifier,
description,
if (hasEquals) GenerateEqualsOrHashCodeFix.HashCode else GenerateEqualsOrHashCodeFix.Equals
)
}
else -> return
}
}
}
}
}
@@ -0,0 +1,42 @@
<problems>
<problem>
<file>test.kt</file>
<line>1</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="temp:///src/test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">equals() and hashCode() not paired</problem_class>
<description>Class has &lt;code&gt;equals()&lt;/code&gt; defined but does not define &lt;code&gt;hashCode()&lt;/code&gt;</description>
</problem>
<problem>
<file>test.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="temp:///src/test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">equals() and hashCode() not paired</problem_class>
<description>Class has &lt;code&gt;hashCode()&lt;/code&gt; defined but does not define &lt;code&gt;equals()&lt;/code&gt;</description>
</problem>
<problem>
<file>test.kt</file>
<line>14</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="temp:///src/test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">equals() and hashCode() not paired</problem_class>
<description>equals()/hashCode() in object declaration</description>
</problem>
<problem>
<file>test.kt</file>
<line>18</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="temp:///src/test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">equals() and hashCode() not paired</problem_class>
<description>equals()/hashCode() in object declaration</description>
</problem>
<problem>
<file>test.kt</file>
<line>22</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="temp:///src/test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">equals() and hashCode() not paired</problem_class>
<description>equals()/hashCode() in object declaration</description>
</problem>
</problems>
@@ -0,0 +1 @@
// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.EqualsOrHashCodeInspection
+39
View File
@@ -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
}
@@ -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");