From d710a02263606db0b92644541ca47b2e9b93c7f0 Mon Sep 17 00:00:00 2001 From: Pavel Kirpichenkov Date: Wed, 10 Jul 2024 18:02:24 +0300 Subject: [PATCH] [Kotlin] Consider supertypes in 'array in data class' quickfix KTIJ-28464 GitOrigin-RevId: 35c20e8401be48af0cc166b38f7603b4fbf62f73 --- .../GenerateEqualsAndHashCodeUtils.kt | 43 +++++++++++++----- .../tests/K2LocalInspectionTestGenerated.java | 15 +++++++ .../LocalInspectionTestGenerated.java | 15 +++++++ .../arrayInDataClass/blankSupertypes.kt | 10 +++++ .../arrayInDataClass/blankSupertypes.kt.after | 25 +++++++++++ .../changedParameterNameInParent.kt | 26 +++++++++++ .../changedParameterNameInParent.kt.after | 44 +++++++++++++++++++ .../arrayInDataClass/indirectOverride.kt | 11 +++++ .../indirectOverride.kt.after | 27 ++++++++++++ 9 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt.after diff --git a/plugins/kotlin/code-insight/impl-base/src/org/jetbrains/kotlin/idea/codeinsights/impl/base/intentions/GenerateEqualsAndHashCodeUtils.kt b/plugins/kotlin/code-insight/impl-base/src/org/jetbrains/kotlin/idea/codeinsights/impl/base/intentions/GenerateEqualsAndHashCodeUtils.kt index d111f0906a3d..9cad6a74d5f1 100644 --- a/plugins/kotlin/code-insight/impl-base/src/org/jetbrains/kotlin/idea/codeinsights/impl/base/intentions/GenerateEqualsAndHashCodeUtils.kt +++ b/plugins/kotlin/code-insight/impl-base/src/org/jetbrains/kotlin/idea/codeinsights/impl/base/intentions/GenerateEqualsAndHashCodeUtils.kt @@ -231,25 +231,32 @@ object GenerateEqualsAndHashCodeUtils { context(KaSession) private fun findEqualsMethodForClass(classSymbol: KaClassSymbol): KaCallableSymbol? = - findMethodInMemberScopeOrInAny(classSymbol, EQUALS) { matchesEqualsMethodSignature(it) } + findNonGeneratedMethodInSelfOrSuperclass(classSymbol, EQUALS) { matchesEqualsMethodSignature(it) } context(KaSession) private fun findHashCodeMethodForClass(classSymbol: KaClassSymbol): KaCallableSymbol? = - findMethodInMemberScopeOrInAny(classSymbol, HASH_CODE) { matchesHashCodeMethodSignature(it) } + findNonGeneratedMethodInSelfOrSuperclass(classSymbol, HASH_CODE) { matchesHashCodeMethodSignature(it) } + /** + * Searches for a callable member symbol with the given [methodName] that matches the [signatureFilter]. + * If the found symbol is generated, the search is done for one more time in the superclass' scope + */ context(KaSession) - private fun findMethodInMemberScopeOrInAny( + private fun findNonGeneratedMethodInSelfOrSuperclass( classSymbol: KaClassSymbol, methodName: Name, signatureFilter: (KaNamedFunctionSymbol) -> Boolean ): KaCallableSymbol? { - findMethod(classSymbol, methodName) { callableSymbol -> - if (callableSymbol !is KaNamedFunctionSymbol) return@findMethod false - signatureFilter(callableSymbol) && callableSymbol.origin != KaSymbolOrigin.SOURCE_MEMBER_GENERATED - }?.let { return it } + val methodSymbol = findMethod(classSymbol, methodName, signatureFilter) - val anySuperClassSymbol = classSymbol.superTypes.find { it.isAnyType }?.symbol as? KaClassSymbol ?: return null - return anySuperClassSymbol.memberScope.callables(methodName).singleOrNull() + // We are not interested in synthetic members of data classes here + // They won't be generated by the compiler after the explicit members are created (see the Kotlin Specification, 4.1.2) + if (methodSymbol?.origin != KaSymbolOrigin.SOURCE_MEMBER_GENERATED) return methodSymbol + + // Instead, if a generated member was found, we search for a member again in its parent class' scope to find a relevant member + val directSuperclassSymbol = findExplicitSuperclassOrAny(classSymbol) ?: return null + + return findMethod(directSuperclassSymbol, methodName, signatureFilter) } /** @@ -258,8 +265,22 @@ object GenerateEqualsAndHashCodeUtils { */ context(KaSession) private fun findMethod( - classSymbol: KaClassSymbol, methodName: Name, condition: (KaCallableSymbol) -> Boolean - ): KaCallableSymbol? = classSymbol.memberScope.callables(methodName).filter(condition).singleOrNull() + classSymbol: KaClassSymbol, methodName: Name, condition: (KaNamedFunctionSymbol) -> Boolean + ): KaCallableSymbol? = classSymbol.memberScope.callables(methodName).filter { + it is KaNamedFunctionSymbol && condition(it) + }.singleOrNull() + + /** + * Searches for the direct superclass symbol of this [classSymbol] (ignoring interfaces). + * Because currently `kotlin.Any` is not listed in symbol's supertypes when all declared supertypes are interfaces, + * this case is handled separately. + */ + context(KaSession) + private fun findExplicitSuperclassOrAny(classSymbol: KaClassSymbol): KaClassSymbol? { + val supertypes = classSymbol.superTypes + return supertypes.map { it.symbol }.filterIsInstance().singleOrNull { it.classKind == KaClassKind.CLASS } + ?: supertypes.first().allSupertypes.singleOrNull { it.isAnyType }?.symbol as? KaClassSymbol + } /** * A function to generate the "not equals" comparison between the class of `this` and the class of the parameter. diff --git a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java index cec9c4b1cd62..6462089d4832 100644 --- a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java +++ b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java @@ -6661,11 +6661,26 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); } + @TestMetadata("blankSupertypes.kt") + public void testBlankSupertypes() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt"); + } + + @TestMetadata("changedParameterNameInParent.kt") + public void testChangedParameterNameInParent() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt"); + } + @TestMetadata("genericArray.kt") public void testGenericArray() throws Exception { runTest("../../../idea/tests/testData/inspectionsLocal/arrayInDataClass/genericArray.kt"); } + @TestMetadata("indirectOverride.kt") + public void testIndirectOverride() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt"); + } + @TestMetadata("intArray.kt") public void testIntArray() throws Exception { runTest("../../../idea/tests/testData/inspectionsLocal/arrayInDataClass/intArray.kt"); diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index 67a3cc476bf7..7e53b7d52748 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -82,11 +82,26 @@ public abstract class LocalInspectionTestGenerated extends AbstractLocalInspecti KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); } + @TestMetadata("blankSupertypes.kt") + public void testBlankSupertypes() throws Exception { + runTest("testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt"); + } + + @TestMetadata("changedParameterNameInParent.kt") + public void testChangedParameterNameInParent() throws Exception { + runTest("testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt"); + } + @TestMetadata("genericArray.kt") public void testGenericArray() throws Exception { runTest("testData/inspectionsLocal/arrayInDataClass/genericArray.kt"); } + @TestMetadata("indirectOverride.kt") + public void testIndirectOverride() throws Exception { + runTest("testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt"); + } + @TestMetadata("intArray.kt") public void testIntArray() throws Exception { runTest("testData/inspectionsLocal/arrayInDataClass/intArray.kt"); diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt new file mode 100644 index 000000000000..c2e430a87a6a --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt @@ -0,0 +1,10 @@ +// WITH_STDLIB + +interface I1 +interface I2 +interface I3 : I2 + +open class C +open class D : C() + +data class A(val a: IntArray): D(), I1, I3 \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt.after new file mode 100644 index 000000000000..989b1fec4bb3 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/blankSupertypes.kt.after @@ -0,0 +1,25 @@ +// WITH_STDLIB + +interface I1 +interface I2 +interface I3 : I2 + +open class C +open class D : C() + +data class A(val a: IntArray): D(), I1, I3 { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as A + + if (!a.contentEquals(other.a)) return false + + return true + } + + override fun hashCode(): Int { + return a.contentHashCode() + } +} diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt new file mode 100644 index 000000000000..d7023a56cf50 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt @@ -0,0 +1,26 @@ +// WITH_STDLIB + +data class A(val a: IntArray) : Parent("some", intArrayOf(1, 2, 3)) + +open class Parent( + val some: String, + val array: IntArray, +) { + override fun equals(renamed: Any?): Boolean { + if (this === renamed) return true + if (javaClass != renamed?.javaClass) return false + + renamed as Parent + + if (some != renamed.some) return false + if (!array.contentEquals(renamed.array)) return false + + return true + } + + override fun hashCode(): Int { + var result = some.hashCode() + result = 31 * result + array.contentHashCode() + return result + } +} diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt.after new file mode 100644 index 000000000000..72da3ae8721a --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/changedParameterNameInParent.kt.after @@ -0,0 +1,44 @@ +// WITH_STDLIB + +data class A(val a: IntArray) : Parent("some", intArrayOf(1, 2, 3)) { + override fun equals(renamed: Any?): Boolean { + if (this === renamed) return true + if (javaClass != renamed?.javaClass) return false + if (!super.equals(renamed)) return false + + renamed as A + + if (!a.contentEquals(renamed.a)) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + a.contentHashCode() + return result + } +} + +open class Parent( + val some: String, + val array: IntArray, +) { + override fun equals(renamed: Any?): Boolean { + if (this === renamed) return true + if (javaClass != renamed?.javaClass) return false + + renamed as Parent + + if (some != renamed.some) return false + if (!array.contentEquals(renamed.array)) return false + + return true + } + + override fun hashCode(): Int { + var result = some.hashCode() + result = 31 * result + array.contentHashCode() + return result + } +} diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt new file mode 100644 index 000000000000..52f8e20909d2 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt @@ -0,0 +1,11 @@ +// WITH_STDLIB + +open class GrandParent { + override fun equals(renamed: Any?): Boolean { + return super.equals(renamed) + } +} + +open class Parent() : GrandParent() + +data class A(val a: IntArray) : Parent() diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt.after new file mode 100644 index 000000000000..f9a2f1decd1f --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/arrayInDataClass/indirectOverride.kt.after @@ -0,0 +1,27 @@ +// WITH_STDLIB + +open class GrandParent { + override fun equals(renamed: Any?): Boolean { + return super.equals(renamed) + } +} + +open class Parent() : GrandParent() + +data class A(val a: IntArray) : Parent() { + override fun equals(renamed: Any?): Boolean { + if (this === renamed) return true + if (javaClass != renamed?.javaClass) return false + if (!super.equals(renamed)) return false + + renamed as A + + if (!a.contentEquals(renamed.a)) return false + + return true + } + + override fun hashCode(): Int { + return a.contentHashCode() + } +}