Generate equals/hashCode(): Use class literals when possible
#KT-18683 Fixed
This commit is contained in:
+36
-6
@@ -24,6 +24,7 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiNameIdentifierOwner
|
||||
import com.intellij.util.IncorrectOperationException
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.config.LanguageFeature
|
||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.VariableDescriptor
|
||||
@@ -34,11 +35,15 @@ import org.jetbrains.kotlin.idea.core.CollectingNameValidator
|
||||
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
|
||||
import org.jetbrains.kotlin.idea.core.insertMembersAfter
|
||||
import org.jetbrains.kotlin.idea.core.quoteIfNeeded
|
||||
import org.jetbrains.kotlin.idea.project.languageVersionSettings
|
||||
import org.jetbrains.kotlin.idea.project.platform
|
||||
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.js.resolve.JsPlatform
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.TargetPlatform
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
@@ -134,7 +139,27 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateEquals(project: Project, info: Info): KtNamedFunction? {
|
||||
private fun generateClassLiteralsNotEqual(paramName: String, targetClass: KtClassOrObject): String {
|
||||
val defaultExpression = "$paramName?.javaClass != javaClass"
|
||||
if (!targetClass.languageVersionSettings.supportsFeature(LanguageFeature.BoundCallableReferences)) return defaultExpression
|
||||
return when (targetClass.platform) {
|
||||
is JsPlatform -> "other == null || $paramName::class.js != this::class.js"
|
||||
is TargetPlatform.Default -> "other == null || $paramName::class != this::class"
|
||||
else -> defaultExpression
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateClassLiteral(targetClass: KtClassOrObject): String {
|
||||
val defaultExpression = "javaClass"
|
||||
if (!targetClass.languageVersionSettings.supportsFeature(LanguageFeature.BoundCallableReferences)) return defaultExpression
|
||||
return when (targetClass.platform) {
|
||||
is JsPlatform -> "this::class.js"
|
||||
is TargetPlatform.Default -> "this::class"
|
||||
else -> defaultExpression
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateEquals(project: Project, info: Info, targetClass: KtClassOrObject): KtNamedFunction? {
|
||||
with(info) {
|
||||
if (!needEquals) return null
|
||||
|
||||
@@ -149,7 +174,12 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
|
||||
}
|
||||
|
||||
val useIsCheck = CodeInsightSettings.getInstance().USE_INSTANCEOF_ON_EQUALS_PARAMETER
|
||||
val isNotInstanceCondition = if (useIsCheck) "$paramName !is $typeForCast" else "$paramName?.javaClass != javaClass"
|
||||
val isNotInstanceCondition = if (useIsCheck) {
|
||||
"$paramName !is $typeForCast"
|
||||
}
|
||||
else {
|
||||
generateClassLiteralsNotEqual(paramName, targetClass)
|
||||
}
|
||||
val bodyText = StringBuilder().apply {
|
||||
append("if (this === $paramName) return true\n")
|
||||
append("if ($isNotInstanceCondition) return false\n")
|
||||
@@ -189,7 +219,7 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateHashCode(project: Project, info: Info): KtNamedFunction? {
|
||||
private fun generateHashCode(project: Project, info: Info, targetClass: KtClassOrObject): KtNamedFunction? {
|
||||
fun VariableDescriptor.genVariableHashCode(parenthesesNeeded: Boolean): String {
|
||||
val ref = (DescriptorToSourceUtilsIde.getAnyDeclaration(project, this) as PsiNameIdentifierOwner).nameIdentifier!!.text
|
||||
val isNullable = TypeUtils.isNullableType(type)
|
||||
@@ -226,7 +256,7 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
|
||||
val initialValue = when {
|
||||
!builtins.isMemberOfAny(superHashCode) -> "super.hashCode()"
|
||||
propertyIterator.hasNext() -> propertyIterator.next().genVariableHashCode(false)
|
||||
else -> "javaClass.hashCode()"
|
||||
else -> generateClassLiteral(targetClass) + ".hashCode()"
|
||||
}
|
||||
|
||||
val bodyText = if (propertyIterator.hasNext()) {
|
||||
@@ -249,8 +279,8 @@ class KotlinGenerateEqualsAndHashcodeAction : KotlinGenerateMemberActionBase<Kot
|
||||
val targetClass = info.classDescriptor.source.getPsi() as KtClass
|
||||
val prototypes = ArrayList<KtDeclaration>(2)
|
||||
.apply {
|
||||
addIfNotNull(generateEquals(project, info))
|
||||
addIfNotNull(generateHashCode(project, info))
|
||||
addIfNotNull(generateEquals(project, info, targetClass))
|
||||
addIfNotNull(generateHashCode(project, info, targetClass))
|
||||
}
|
||||
val anchor = with(targetClass.declarations) { lastIsInstanceOrNull<KtNamedFunction>() ?: lastOrNull() }
|
||||
return insertMembersAfter(editor, targetClass, prototypes, anchor)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// PLATFORM: Common
|
||||
class A(val n: Int, val s: String) {<caret>
|
||||
val f: Float = 1.0f
|
||||
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// PLATFORM: Common
|
||||
class A(val n: Int, val s: String) {
|
||||
val f: Float = 1.0f
|
||||
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
|
||||
<caret>override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other::class != this::class) return false
|
||||
|
||||
other as A
|
||||
|
||||
if (n != other.n) return false
|
||||
if (s != other.s) return false
|
||||
if (f != other.f) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = n
|
||||
result = 31 * result + s.hashCode()
|
||||
result = 31 * result + f.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// PLATFORM: JavaScript
|
||||
class A(val n: Int, val s: String) {<caret>
|
||||
val f: Float = 1.0f
|
||||
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// PLATFORM: JavaScript
|
||||
class A(val n: Int, val s: String) {
|
||||
val f: Float = 1.0f
|
||||
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
|
||||
<caret>override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other::class.js != this::class.js) return false
|
||||
|
||||
other as A
|
||||
|
||||
if (n != other.n) return false
|
||||
if (s != other.s) return false
|
||||
if (f != other.f) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = n
|
||||
result = 31 * result + s.hashCode()
|
||||
result = 31 * result + f.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// PLATFORM: Common
|
||||
class A {<caret>
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// PLATFORM: Common
|
||||
class A {
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
|
||||
<caret>override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other::class != this::class) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return this::class.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// PLATFORM: JavaScript
|
||||
class A {<caret>
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// PLATFORM: JavaScript
|
||||
class A {
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
|
||||
<caret>override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other::class.js != this::class.js) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return this::class.js.hashCode()
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -28,6 +28,11 @@ import junit.framework.TestCase
|
||||
import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil
|
||||
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
|
||||
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
|
||||
import org.jetbrains.kotlin.js.resolve.JsPlatform
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.targetPlatform
|
||||
import org.jetbrains.kotlin.resolve.TargetPlatform
|
||||
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform
|
||||
import org.jetbrains.kotlin.test.InTextDirectivesUtils
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import java.io.File
|
||||
@@ -61,6 +66,8 @@ abstract class AbstractCodeInsightActionTest : KotlinLightCodeInsightFixtureTest
|
||||
val conflictFile = File("$path.messages")
|
||||
val afterFile = File("$path.after")
|
||||
|
||||
var mainPsiFile: KtFile? = null
|
||||
|
||||
try {
|
||||
ConfigLibraryUtil.configureLibrariesByDirective(myModule, PlatformTestUtil.getCommunityPath(), fileText)
|
||||
|
||||
@@ -76,7 +83,18 @@ abstract class AbstractCodeInsightActionTest : KotlinLightCodeInsightFixtureTest
|
||||
myFixture.configureByFile(File(rootDir, it).path.replace(File.separator, "/"))
|
||||
}
|
||||
configureExtra(path, fileText)
|
||||
myFixture.configureByFile(path)
|
||||
mainPsiFile = myFixture.configureByFile(path) as KtFile
|
||||
|
||||
val targetPlatformName = InTextDirectivesUtils.findStringWithPrefixes(fileText, "// PLATFORM: ")
|
||||
if (targetPlatformName != null) {
|
||||
val targetPlatform = when (targetPlatformName) {
|
||||
"JVM" -> JvmPlatform
|
||||
"JavaScript" -> JsPlatform
|
||||
"Common" -> TargetPlatform.Default
|
||||
else -> error("Unexpected platform name: $targetPlatformName")
|
||||
}
|
||||
mainPsiFile.targetPlatform = targetPlatform
|
||||
}
|
||||
|
||||
val action = createAction(fileText)
|
||||
|
||||
@@ -103,6 +121,7 @@ abstract class AbstractCodeInsightActionTest : KotlinLightCodeInsightFixtureTest
|
||||
KotlinTestUtils.assertEqualsToFile(conflictFile, e.message!!)
|
||||
}
|
||||
finally {
|
||||
mainPsiFile?.targetPlatform = null
|
||||
ConfigLibraryUtil.unconfigureLibrariesByDirective(myModule, fileText)
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -108,6 +108,18 @@ public class GenerateHashCodeAndEqualsActionTestGenerated extends AbstractGenera
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("multipleVarsCommon.kt")
|
||||
public void testMultipleVarsCommon() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/multipleVarsCommon.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("multipleVarsJS.kt")
|
||||
public void testMultipleVarsJS() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/multipleVarsJS.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("multipleVarsNullable.kt")
|
||||
public void testMultipleVarsNullable() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/multipleVarsNullable.kt");
|
||||
@@ -132,6 +144,18 @@ public class GenerateHashCodeAndEqualsActionTestGenerated extends AbstractGenera
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("noVarsCommon.kt")
|
||||
public void testNoVarsCommon() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/noVarsCommon.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("noVarsJS.kt")
|
||||
public void testNoVarsJS() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/noVarsJS.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("noVarsWithSuperClass.kt")
|
||||
public void testNoVarsWithSuperClass() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/generate/equalsWithHashCode/noVarsWithSuperClass.kt");
|
||||
|
||||
Reference in New Issue
Block a user