Inspections: Add inspection on equals()/hashCode()
This commit is contained in:
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
+23
-23
@@ -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 <code>equals()</code> defined but does not define <code>hashCode()</code></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 <code>hashCode()</code> defined but does not define <code>equals()</code></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
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user