Add quick fix to generate equals / hashcode in data class with arrays

So #KT-15893 Fixed
This commit is contained in:
Toshiaki Kameyama
2017-06-14 11:45:33 +03:00
committed by Mikhail Glukhikh
parent 5df5a001a1
commit b9fb7cc3a5
9 changed files with 99 additions and 6 deletions
@@ -27,6 +27,7 @@ import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.VariableDescriptor import org.jetbrains.kotlin.descriptors.VariableDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.core.CollectingNameValidator import org.jetbrains.kotlin.idea.core.CollectingNameValidator
@@ -35,12 +36,12 @@ import org.jetbrains.kotlin.idea.core.insertMembersAfter
import org.jetbrains.kotlin.idea.core.quoteIfNeeded import org.jetbrains.kotlin.idea.core.quoteIfNeeded
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.types.TypeUtils import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.addIfNotNull
@@ -75,10 +76,20 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
&& targetClass !is KtEnumEntry && targetClass !is KtEnumEntry
&& !targetClass.isAnnotation() && !targetClass.isAnnotation()
&& !targetClass.isInterface() && !targetClass.isInterface()
&& !targetClass.hasModifier(KtTokens.DATA_KEYWORD) && (!targetClass.isData() || isValidForDataClass(targetClass))
&& getPropertiesToUseInGeneratedMember(targetClass).isNotEmpty() && getPropertiesToUseInGeneratedMember(targetClass).isNotEmpty()
} }
private fun isValidForDataClass(targetClass: KtClass): Boolean {
val constructor = targetClass.primaryConstructor ?: return false
val context = constructor.analyze(BodyResolveMode.PARTIAL)
return constructor.valueParameters.any { parameter ->
parameter.hasValOrVar() && context.get(BindingContext.TYPE, parameter.typeReference)?.let { type ->
KotlinBuiltIns.isArray(type) || KotlinBuiltIns.isPrimitiveArray(type)
} ?: false
}
}
override fun prepareMembersInfo(klass: KtClassOrObject, project: Project, editor: Editor?): Info? { override fun prepareMembersInfo(klass: KtClassOrObject, project: Project, editor: Editor?): Info? {
if (klass !is KtClass) throw AssertionError("Not a class: ${klass.getElementTextWithContext()}") if (klass !is KtClass) throw AssertionError("Not a class: ${klass.getElementTextWithContext()}")
@@ -16,16 +16,18 @@
package org.jetbrains.kotlin.idea.inspections package org.jetbrains.kotlin.idea.inspections
import com.intellij.codeInspection.LocalInspectionToolSession import com.intellij.codeInsight.FileModificationService
import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.*
import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiElementVisitor
import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsAndHashcodeAction
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtVisitorVoid import org.jetbrains.kotlin.psi.KtVisitorVoid
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.util.OperatorNameConventions import org.jetbrains.kotlin.util.OperatorNameConventions
@@ -44,7 +46,8 @@ class ArrayInDataClassInspection : AbstractKotlinInspection() {
if (KotlinBuiltIns.isArray(type) || KotlinBuiltIns.isPrimitiveArray(type)) { if (KotlinBuiltIns.isArray(type) || KotlinBuiltIns.isPrimitiveArray(type)) {
holder.registerProblem(parameter, holder.registerProblem(parameter,
"Array property in data class: it's recommended to override equals() / hashCode()", "Array property in data class: it's recommended to override equals() / hashCode()",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING) ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
GenerateEqualsAndHashcodeFix())
} }
} }
} }
@@ -71,4 +74,19 @@ class ArrayInDataClassInspection : AbstractKotlinInspection() {
} }
} }
} }
class GenerateEqualsAndHashcodeFix : LocalQuickFix {
override fun getName() = "Generate equals() and hashCode()"
override fun getFamilyName() = name
override fun startInWriteAction() = false
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
if (!FileModificationService.getInstance().preparePsiElementForWrite(descriptor.psiElement)) return
descriptor.psiElement.getNonStrictParentOfType<KtClass>()?.run {
KotlinGenerateEqualsAndHashcodeAction().doInvoke(project, descriptor.psiElement.findExistingEditor(), this)
}
}
}
} }
@@ -0,0 +1 @@
data class A<caret>(val a: IntArray)
@@ -0,0 +1,18 @@
import java.util.Arrays
data class A(val a: IntArray) {
<caret>override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as A
if (!Arrays.equals(a, other.a)) return false
return true
}
override fun hashCode(): Int {
return Arrays.hashCode(a)
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.ArrayInDataClassInspection
@@ -0,0 +1,3 @@
// WITH_RUNTIME
data class A(<caret>val a: IntArray)
@@ -0,0 +1,20 @@
import java.util.Arrays
// WITH_RUNTIME
data class A(val a: IntArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as A
if (!Arrays.equals(a, other.a)) return false
return true
}
override fun hashCode(): Int {
return Arrays.hashCode(a)
}
}
@@ -60,6 +60,12 @@ public class GenerateHashCodeAndEqualsActionTestGenerated extends AbstractGenera
doTest(fileName); doTest(fileName);
} }
@TestMetadata("dataClassHasArrayProperty.kt")
public void testDataClassHasArrayProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/dataClassHasArrayProperty.kt");
doTest(fileName);
}
@TestMetadata("enum.kt") @TestMetadata("enum.kt")
public void testEnum() throws Exception { public void testEnum() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/enum.kt"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/enum.kt");
@@ -36,6 +36,21 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
} }
@TestMetadata("idea/testData/inspectionsLocal/arrayInDataClass")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ArrayInDataClass extends AbstractLocalInspectionTest {
public void testAllFilesPresentInArrayInDataClass() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/arrayInDataClass"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("test.kt")
public void testTest() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/arrayInDataClass/test.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/inspectionsLocal/copyWithoutNamedArguments") @TestMetadata("idea/testData/inspectionsLocal/copyWithoutNamedArguments")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)