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"
|
cleanupTool="true"
|
||||||
level="WARNING"/>
|
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"/>
|
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||||
|
|
||||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
<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 org.jetbrains.kotlin.utils.addToStdlib.lastIsInstanceOrNull
|
||||||
import java.util.*
|
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>() {
|
class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<KotlinGenerateEqualsAndHashcodeAction.Info>() {
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG = Logger.getInstance(KotlinGenerateEqualsAndHashcodeAction::class.java)
|
private val LOG = Logger.getInstance(KotlinGenerateEqualsAndHashcodeAction::class.java)
|
||||||
@@ -75,29 +98,6 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
|
|||||||
&& targetClass.getPropertiesToUse().isNotEmpty()
|
&& 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> {
|
private fun JetClassOrObject.getPropertiesToUse(): List<JetNamedDeclaration> {
|
||||||
return ArrayList<JetNamedDeclaration>().apply {
|
return ArrayList<JetNamedDeclaration>().apply {
|
||||||
getPrimaryConstructorParameters().filterTo(this) { it.hasValOrVar() }
|
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);
|
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")
|
@TestMetadata("redundantSamConstructor/inspectionData/inspections.test")
|
||||||
public void testRedundantSamConstructor_inspectionData_Inspections_test() throws Exception {
|
public void testRedundantSamConstructor_inspectionData_Inspections_test() throws Exception {
|
||||||
String fileName = JetTestUtils.navigationMetadata("idea/testData/inspections/redundantSamConstructor/inspectionData/inspections.test");
|
String fileName = JetTestUtils.navigationMetadata("idea/testData/inspections/redundantSamConstructor/inspectionData/inspections.test");
|
||||||
|
|||||||
Reference in New Issue
Block a user