mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[Kotlin] Add a scope object for util functions
KTIJ-28464 GitOrigin-RevId: 250b769e60ee1e88e14902c9fd713436876236d5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
63d99e08a4
commit
cf4c3f5d8b
+273
-271
@@ -28,300 +28,302 @@ import org.jetbrains.kotlin.psi.psiUtil.quoteIfNeeded
|
||||
import org.jetbrains.kotlin.util.OperatorNameConventions.EQUALS
|
||||
import org.jetbrains.kotlin.util.OperatorNameConventions.HASH_CODE
|
||||
|
||||
context(KaSession)
|
||||
fun matchesEqualsMethodSignature(function: KaNamedFunctionSymbol): Boolean {
|
||||
if (function.modality == KaSymbolModality.ABSTRACT) return false
|
||||
if (function.name != EQUALS) return false
|
||||
if (function.typeParameters.isNotEmpty()) return false
|
||||
val param = function.valueParameters.singleOrNull() ?: return false
|
||||
val paramType = param.returnType
|
||||
val returnType = function.returnType
|
||||
return paramType.isNullableAnyType() && returnType.isNonNullableBooleanType()
|
||||
}
|
||||
|
||||
context(KaSession)
|
||||
fun matchesHashCodeMethodSignature(function: KaNamedFunctionSymbol): Boolean {
|
||||
if (function.modality == KaSymbolModality.ABSTRACT) return false
|
||||
if (function.name != HASH_CODE) return false
|
||||
if (function.typeParameters.isNotEmpty()) return false
|
||||
if (function.valueParameters.isNotEmpty()) return false
|
||||
val returnType = function.returnType
|
||||
return returnType.isIntType && !returnType.isMarkedNullable
|
||||
}
|
||||
|
||||
fun generateEqualsHeaderAndBodyTexts(targetClass: KtClass): Pair<String, String> {
|
||||
val hasExpectModifier = targetClass.hasExpectModifier()
|
||||
var functionText = buildString {
|
||||
append("override fun equals(other: Any?): Boolean")
|
||||
if (!hasExpectModifier) {
|
||||
append(" {\n return super.equals(other)\n}")
|
||||
}
|
||||
object GenerateEqualsAndHashCodeUtils {
|
||||
context(KaSession)
|
||||
fun matchesEqualsMethodSignature(function: KaNamedFunctionSymbol): Boolean {
|
||||
if (function.modality == KaSymbolModality.ABSTRACT) return false
|
||||
if (function.name != EQUALS) return false
|
||||
if (function.typeParameters.isNotEmpty()) return false
|
||||
val param = function.valueParameters.singleOrNull() ?: return false
|
||||
val paramType = param.returnType
|
||||
val returnType = function.returnType
|
||||
return paramType.isNullableAnyType() && returnType.isNonNullableBooleanType()
|
||||
}
|
||||
var bodyText = ""
|
||||
analyze(targetClass) {
|
||||
val classSymbol = targetClass.classSymbol ?: return@analyze
|
||||
val equalsMethod = findEqualsMethodForClass(classSymbol) as? KaNamedFunctionSymbol ?: return@analyze
|
||||
val superContainingEqualsMethod = equalsMethod.containingDeclaration ?: return@analyze
|
||||
|
||||
val parameterName = equalsMethod.valueParameters.singleOrNull()?.name?.asString() ?: return@analyze
|
||||
context(KaSession)
|
||||
fun matchesHashCodeMethodSignature(function: KaNamedFunctionSymbol): Boolean {
|
||||
if (function.modality == KaSymbolModality.ABSTRACT) return false
|
||||
if (function.name != HASH_CODE) return false
|
||||
if (function.typeParameters.isNotEmpty()) return false
|
||||
if (function.valueParameters.isNotEmpty()) return false
|
||||
val returnType = function.returnType
|
||||
return returnType.isIntType && !returnType.isMarkedNullable
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Correctly emits parameter type and return type when there are user-defined classes "Any" and "Boolean".
|
||||
* The counterpart in FE1.0 uses [org.jetbrains.kotlin.idea.actions.generate.generateFunctionSkeleton] that automatically
|
||||
* generates `equals(other: Any?)` by default and it generates `equals(other: kotlin.Any?)` if the project has a custom class
|
||||
* named "Any". To preserve the same behavior for FIR, we tried
|
||||
* [org.jetbrains.kotlin.analysis.api.components.KtSymbolDeclarationRendererMixIn.render] with
|
||||
* [org.jetbrains.kotlin.analysis.api.components.KaBuiltinTypes.any],
|
||||
* `builtinTypes.ANY.expandedClassSymbol.classId?.asSingleFqName()?.asString()`, and
|
||||
* `org.jetbrains.kotlin.builtins.StandardNames.FqNames.any.render()`, but they all have the same rendering result
|
||||
* regardless of the user-defined "Any" class. We specify the parameter and the return type as `kotlin.Any?` and
|
||||
* `kotlin.Boolean` and rely on [org.jetbrains.kotlin.idea.base.analysis.api.utils.shortenReferences], but it always
|
||||
* shortens the name regardless of the existence of the user-defined classes.
|
||||
*/
|
||||
functionText = buildString {
|
||||
append("override fun equals(${parameterName}: kotlin.Any?): kotlin.Boolean")
|
||||
fun generateEqualsHeaderAndBodyTexts(targetClass: KtClass): Pair<String, String> {
|
||||
val hasExpectModifier = targetClass.hasExpectModifier()
|
||||
var functionText = buildString {
|
||||
append("override fun equals(other: Any?): Boolean")
|
||||
if (!hasExpectModifier) {
|
||||
append(" {\n return super.equals(${parameterName})\n}")
|
||||
append(" {\n return super.equals(other)\n}")
|
||||
}
|
||||
}
|
||||
var bodyText = ""
|
||||
analyze(targetClass) {
|
||||
val classSymbol = targetClass.classSymbol ?: return@analyze
|
||||
val equalsMethod = findEqualsMethodForClass(classSymbol) as? KaNamedFunctionSymbol ?: return@analyze
|
||||
val superContainingEqualsMethod = equalsMethod.containingDeclaration ?: return@analyze
|
||||
|
||||
var typeForCast = targetClass.classIdIfNonLocal?.asSingleFqName()?.asString()
|
||||
val typeParams = targetClass.typeParameters
|
||||
if (typeParams.isNotEmpty()) {
|
||||
typeForCast += typeParams.joinToString(prefix = "<", postfix = ">") { "*" }
|
||||
}
|
||||
|
||||
val useIsCheck = CodeInsightSettings.getInstance().USE_INSTANCEOF_ON_EQUALS_PARAMETER
|
||||
val isNotInstanceCondition = if (useIsCheck) {
|
||||
"$parameterName !is $typeForCast"
|
||||
} else {
|
||||
generateClassLiteralsNotEqual(parameterName, targetClass)
|
||||
}
|
||||
|
||||
bodyText = buildString {
|
||||
append("if (this === $parameterName) return true\n")
|
||||
append("if ($isNotInstanceCondition) return false\n")
|
||||
if (superContainingEqualsMethod != builtinTypes.any.expandedSymbol) {
|
||||
append("if (!super.equals($parameterName)) return false\n")
|
||||
}
|
||||
val parameterName = equalsMethod.valueParameters.singleOrNull()?.name?.asString() ?: return@analyze
|
||||
|
||||
/**
|
||||
* TODO: We have to add a wizard to select members used for equals. See how
|
||||
* TODO: Correctly emits parameter type and return type when there are user-defined classes "Any" and "Boolean".
|
||||
* The counterpart in FE1.0 uses [org.jetbrains.kotlin.idea.actions.generate.generateFunctionSkeleton] that automatically
|
||||
* generates `equals(other: Any?)` by default and it generates `equals(other: kotlin.Any?)` if the project has a custom class
|
||||
* named "Any". To preserve the same behavior for FIR, we tried
|
||||
* [org.jetbrains.kotlin.analysis.api.components.KtSymbolDeclarationRendererMixIn.render] with
|
||||
* [org.jetbrains.kotlin.analysis.api.components.KaBuiltinTypes.any],
|
||||
* `builtinTypes.ANY.expandedClassSymbol.classId?.asSingleFqName()?.asString()`, and
|
||||
* `org.jetbrains.kotlin.builtins.StandardNames.FqNames.any.render()`, but they all have the same rendering result
|
||||
* regardless of the user-defined "Any" class. We specify the parameter and the return type as `kotlin.Any?` and
|
||||
* `kotlin.Boolean` and rely on [org.jetbrains.kotlin.idea.base.analysis.api.utils.shortenReferences], but it always
|
||||
* shortens the name regardless of the existence of the user-defined classes.
|
||||
*/
|
||||
functionText = buildString {
|
||||
append("override fun equals(${parameterName}: kotlin.Any?): kotlin.Boolean")
|
||||
if (!hasExpectModifier) {
|
||||
append(" {\n return super.equals(${parameterName})\n}")
|
||||
}
|
||||
}
|
||||
|
||||
var typeForCast = targetClass.classIdIfNonLocal?.asSingleFqName()?.asString()
|
||||
val typeParams = targetClass.typeParameters
|
||||
if (typeParams.isNotEmpty()) {
|
||||
typeForCast += typeParams.joinToString(prefix = "<", postfix = ">") { "*" }
|
||||
}
|
||||
|
||||
val useIsCheck = CodeInsightSettings.getInstance().USE_INSTANCEOF_ON_EQUALS_PARAMETER
|
||||
val isNotInstanceCondition = if (useIsCheck) {
|
||||
"$parameterName !is $typeForCast"
|
||||
} else {
|
||||
generateClassLiteralsNotEqual(parameterName, targetClass)
|
||||
}
|
||||
|
||||
bodyText = buildString {
|
||||
append("if (this === $parameterName) return true\n")
|
||||
append("if ($isNotInstanceCondition) return false\n")
|
||||
if (superContainingEqualsMethod != builtinTypes.any.expandedSymbol) {
|
||||
append("if (!super.equals($parameterName)) return false\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: We have to add a wizard to select members used for equals. See how
|
||||
* [org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsAndHashcodeAction.prepareMembersInfo] uses
|
||||
* [org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsWizard].
|
||||
*/
|
||||
val variablesForEquals = getPropertiesToUseInGeneratedMember(targetClass)
|
||||
if (variablesForEquals.isNotEmpty()) {
|
||||
if (!useIsCheck) {
|
||||
append("\n$parameterName as $typeForCast\n")
|
||||
}
|
||||
|
||||
append('\n')
|
||||
|
||||
variablesForEquals.forEach {
|
||||
val variableType = it.returnType
|
||||
val isNullableType = variableType.isMarkedNullable
|
||||
val isArray = variableType.isArrayOrPrimitiveArray
|
||||
val canUseArrayContentFunctions = targetClass.canUseArrayContentFunctions()
|
||||
val propName = it.name ?: return@forEach
|
||||
val notEquals = when {
|
||||
isArray -> {
|
||||
"!${
|
||||
generateArraysEqualsCall(
|
||||
variableType, canUseArrayContentFunctions, propName, "$parameterName.$propName"
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
else -> {
|
||||
"$propName != $parameterName.$propName"
|
||||
}
|
||||
}
|
||||
val equalsCheck = "if ($notEquals) return false\n"
|
||||
if (isArray && isNullableType && canUseArrayContentFunctions) {
|
||||
append("if ($propName != null) {\n")
|
||||
append("if ($parameterName.$propName == null) return false\n")
|
||||
append(equalsCheck)
|
||||
append("} else if ($parameterName.$propName != null) return false\n")
|
||||
} else {
|
||||
append(equalsCheck)
|
||||
}
|
||||
}
|
||||
|
||||
append('\n')
|
||||
}
|
||||
|
||||
append("return true")
|
||||
}
|
||||
}
|
||||
return Pair(functionText, bodyText)
|
||||
}
|
||||
|
||||
fun generateHashCodeHeaderAndBodyTexts(targetClass: KtClass): Pair<String, String> {
|
||||
val hasExpectModifier = targetClass.hasExpectModifier()
|
||||
val functionText = buildString {
|
||||
append("override fun hashCode(): kotlin.Int")
|
||||
if (!hasExpectModifier) {
|
||||
append(" {\n return super.hashCode()\n}")
|
||||
}
|
||||
}
|
||||
var bodyText = ""
|
||||
analyze(targetClass) {
|
||||
fun KtNamedDeclaration.genVariableHashCode(parenthesesNeeded: Boolean): String {
|
||||
val ref = name ?: return "0"
|
||||
val type = returnType
|
||||
val isNullable = type.isMarkedNullable
|
||||
|
||||
var text = when {
|
||||
type.semanticallyEquals(builtinTypes.byte) || type.semanticallyEquals(builtinTypes.short)
|
||||
|| type.semanticallyEquals(builtinTypes.int) -> ref
|
||||
|
||||
type.isArrayOrPrimitiveArray -> {
|
||||
val canUseArrayContentFunctions = targetClass.canUseArrayContentFunctions()
|
||||
val shouldWrapInLet = isNullable && !canUseArrayContentFunctions
|
||||
val hashCodeArg = if (shouldWrapInLet) StandardNames.IMPLICIT_LAMBDA_PARAMETER_NAME.identifier else ref
|
||||
val hashCodeCall = generateArrayHashCodeCall(type, canUseArrayContentFunctions, hashCodeArg)
|
||||
if (shouldWrapInLet) "$ref?.let { $hashCodeCall }" else hashCodeCall
|
||||
}
|
||||
|
||||
else -> if (isNullable) "$ref?.hashCode()" else "$ref.hashCode()"
|
||||
}
|
||||
if (isNullable) {
|
||||
text += " ?: 0"
|
||||
if (parenthesesNeeded) {
|
||||
text = "($text)"
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
val classSymbol = targetClass.classSymbol ?: return@analyze
|
||||
val hashCodeMethod = findHashCodeMethodForClass(classSymbol) as? KaNamedFunctionSymbol ?: return@analyze
|
||||
val superContainingHashCodeMethod = hashCodeMethod.containingDeclaration ?: return@analyze
|
||||
|
||||
/**
|
||||
* TODO: We have to add a wizard to select members used for hashCode. See how
|
||||
* [org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsAndHashcodeAction.prepareMembersInfo] uses
|
||||
* [org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsWizard].
|
||||
*/
|
||||
val variablesForEquals = getPropertiesToUseInGeneratedMember(targetClass)
|
||||
if (variablesForEquals.isNotEmpty()) {
|
||||
if (!useIsCheck) {
|
||||
append("\n$parameterName as $typeForCast\n")
|
||||
}
|
||||
val variablesForHashCode = getPropertiesToUseInGeneratedMember(targetClass)
|
||||
val propertyIterator = variablesForHashCode.iterator()
|
||||
|
||||
append('\n')
|
||||
val initialValue = when {
|
||||
superContainingHashCodeMethod != builtinTypes.any.expandedSymbol -> "super.hashCode()"
|
||||
propertyIterator.hasNext() -> propertyIterator.next().genVariableHashCode(false)
|
||||
else -> generateClassLiteral(targetClass) + ".hashCode()"
|
||||
}
|
||||
|
||||
variablesForEquals.forEach {
|
||||
val variableType = it.returnType
|
||||
val isNullableType = variableType.isMarkedNullable
|
||||
val isArray = variableType.isArrayOrPrimitiveArray
|
||||
val canUseArrayContentFunctions = targetClass.canUseArrayContentFunctions()
|
||||
val propName = it.name ?: return@forEach
|
||||
val notEquals = when {
|
||||
isArray -> {
|
||||
"!${
|
||||
generateArraysEqualsCall(
|
||||
variableType, canUseArrayContentFunctions, propName, "$parameterName.$propName"
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
else -> {
|
||||
"$propName != $parameterName.$propName"
|
||||
}
|
||||
bodyText =
|
||||
if (propertyIterator.hasNext()) { // TODO: Confirm that `variablesForHashCode.map { it.name?.quoteIfNeeded()!! }` is safe here
|
||||
val validator = CollectingNameValidator(variablesForHashCode.map { it.name?.quoteIfNeeded()!! })
|
||||
val resultVarName = KotlinNameSuggester.suggestNameByName("result", validator)
|
||||
buildString {
|
||||
append("var $resultVarName = $initialValue\n")
|
||||
propertyIterator.forEach { append("$resultVarName = 31 * $resultVarName + ${it.genVariableHashCode(true)}\n") }
|
||||
append("return $resultVarName")
|
||||
}
|
||||
val equalsCheck = "if ($notEquals) return false\n"
|
||||
if (isArray && isNullableType && canUseArrayContentFunctions) {
|
||||
append("if ($propName != null) {\n")
|
||||
append("if ($parameterName.$propName == null) return false\n")
|
||||
append(equalsCheck)
|
||||
append("} else if ($parameterName.$propName != null) return false\n")
|
||||
} else {
|
||||
append(equalsCheck)
|
||||
}
|
||||
}
|
||||
} else "return $initialValue"
|
||||
}
|
||||
return Pair(functionText, bodyText)
|
||||
}
|
||||
|
||||
append('\n')
|
||||
context(KaSession)
|
||||
private fun findEqualsMethodForClass(classSymbol: KaClassSymbol): KaCallableSymbol? =
|
||||
findMethodInMemberScopeOrInAny(classSymbol, EQUALS) { matchesEqualsMethodSignature(it) }
|
||||
|
||||
context(KaSession)
|
||||
private fun findHashCodeMethodForClass(classSymbol: KaClassSymbol): KaCallableSymbol? =
|
||||
findMethodInMemberScopeOrInAny(classSymbol, HASH_CODE) { matchesHashCodeMethodSignature(it) }
|
||||
|
||||
context(KaSession)
|
||||
private fun findMethodInMemberScopeOrInAny(
|
||||
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 anySuperClassSymbol = classSymbol.superTypes.find { it.isAnyType }?.symbol as? KaClassSymbol ?: return null
|
||||
return anySuperClassSymbol.memberScope.callables(methodName).singleOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds methods whose name is [methodName] not only from the class [classSymbol] but also its parent classes,
|
||||
* and returns method symbols after filtering them using [condition].
|
||||
*/
|
||||
context(KaSession)
|
||||
private fun findMethod(
|
||||
classSymbol: KaClassSymbol, methodName: Name, condition: (KaCallableSymbol) -> Boolean
|
||||
): KaCallableSymbol? = classSymbol.memberScope.callables(methodName).filter(condition).singleOrNull()
|
||||
|
||||
/**
|
||||
* A function to generate the "not equals" comparison between the class of `this` and the class of the parameter.
|
||||
*/
|
||||
private fun generateClassLiteralsNotEqual(paramName: String, targetClass: KtClassOrObject): String {
|
||||
val defaultExpression = "javaClass != $paramName?.javaClass"
|
||||
if (!targetClass.languageVersionSettings.supportsFeature(LanguageFeature.BoundCallableReferences)) return defaultExpression
|
||||
return when {
|
||||
targetClass.platform.isCommon() -> "other == null || this::class != $paramName::class"
|
||||
targetClass.platform.isJs() -> "other == null || this::class.js != $paramName::class.js"
|
||||
else -> defaultExpression
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to generate the class i.e., `javaClass` or `this::class`.
|
||||
*/
|
||||
private fun generateClassLiteral(targetClass: KtClassOrObject): String {
|
||||
val defaultExpression = "javaClass"
|
||||
if (!targetClass.languageVersionSettings.supportsFeature(LanguageFeature.BoundCallableReferences)) return defaultExpression
|
||||
return when {
|
||||
targetClass.platform.isCommon() -> "this::class"
|
||||
targetClass.platform.isJs() -> "this::class.js"
|
||||
else -> defaultExpression
|
||||
}
|
||||
}
|
||||
|
||||
context(KaSession)
|
||||
private fun getPropertiesToUseInGeneratedMember(classOrObject: KtClassOrObject): List<KtNamedDeclaration> =
|
||||
buildList<KtNamedDeclaration> {
|
||||
classOrObject.primaryConstructorParameters.filterTo(this) { it.hasValOrVar() }
|
||||
classOrObject.declarations.asSequence().filterIsInstance<KtProperty>().filterTo(this) {
|
||||
it.symbol is KaPropertySymbol
|
||||
}
|
||||
|
||||
append("return true")
|
||||
}
|
||||
}
|
||||
return Pair(functionText, bodyText)
|
||||
}
|
||||
|
||||
fun generateHashCodeHeaderAndBodyTexts(targetClass: KtClass): Pair<String, String> {
|
||||
val hasExpectModifier = targetClass.hasExpectModifier()
|
||||
val functionText = buildString {
|
||||
append("override fun hashCode(): kotlin.Int")
|
||||
if (!hasExpectModifier) {
|
||||
append(" {\n return super.hashCode()\n}")
|
||||
}
|
||||
}
|
||||
var bodyText = ""
|
||||
analyze(targetClass) {
|
||||
fun KtNamedDeclaration.genVariableHashCode(parenthesesNeeded: Boolean): String {
|
||||
val ref = name ?: return "0"
|
||||
val type = returnType
|
||||
val isNullable = type.isMarkedNullable
|
||||
|
||||
var text = when {
|
||||
type.semanticallyEquals(builtinTypes.byte) || type.semanticallyEquals(builtinTypes.short)
|
||||
|| type.semanticallyEquals(builtinTypes.int) -> ref
|
||||
|
||||
type.isArrayOrPrimitiveArray -> {
|
||||
val canUseArrayContentFunctions = targetClass.canUseArrayContentFunctions()
|
||||
val shouldWrapInLet = isNullable && !canUseArrayContentFunctions
|
||||
val hashCodeArg = if (shouldWrapInLet) StandardNames.IMPLICIT_LAMBDA_PARAMETER_NAME.identifier else ref
|
||||
val hashCodeCall = generateArrayHashCodeCall(type, canUseArrayContentFunctions, hashCodeArg)
|
||||
if (shouldWrapInLet) "$ref?.let { $hashCodeCall }" else hashCodeCall
|
||||
}
|
||||
|
||||
else -> if (isNullable) "$ref?.hashCode()" else "$ref.hashCode()"
|
||||
}
|
||||
if (isNullable) {
|
||||
text += " ?: 0"
|
||||
if (parenthesesNeeded) {
|
||||
text = "($text)"
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}.filter {
|
||||
it.name?.quoteIfNeeded().isIdentifier()
|
||||
}
|
||||
|
||||
val classSymbol = targetClass.classSymbol ?: return@analyze
|
||||
val hashCodeMethod = findHashCodeMethodForClass(classSymbol) as? KaNamedFunctionSymbol ?: return@analyze
|
||||
val superContainingHashCodeMethod = hashCodeMethod.containingDeclaration ?: return@analyze
|
||||
private fun KtElement.canUseArrayContentFunctions() = languageVersionSettings.apiVersion >= ApiVersion.KOTLIN_1_1
|
||||
|
||||
/**
|
||||
* TODO: We have to add a wizard to select members used for hashCode. See how
|
||||
* [org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsAndHashcodeAction.prepareMembersInfo] uses
|
||||
* [org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateEqualsWizard].
|
||||
*/
|
||||
val variablesForHashCode = getPropertiesToUseInGeneratedMember(targetClass)
|
||||
val propertyIterator = variablesForHashCode.iterator()
|
||||
|
||||
val initialValue = when {
|
||||
superContainingHashCodeMethod != builtinTypes.any.expandedSymbol -> "super.hashCode()"
|
||||
propertyIterator.hasNext() -> propertyIterator.next().genVariableHashCode(false)
|
||||
else -> generateClassLiteral(targetClass) + ".hashCode()"
|
||||
context(KaSession)
|
||||
private fun generateArraysEqualsCall(
|
||||
type: KaType, canUseContentFunctions: Boolean, arg1: String, arg2: String
|
||||
): String {
|
||||
return if (canUseContentFunctions) {
|
||||
val methodName = if (type.isNestedArray) "contentDeepEquals" else "contentEquals"
|
||||
"$arg1.$methodName($arg2)"
|
||||
} else {
|
||||
val methodName = if (type.isNestedArray) "deepEquals" else "equals"
|
||||
"java.util.Arrays.$methodName($arg1, $arg2)"
|
||||
}
|
||||
|
||||
bodyText =
|
||||
if (propertyIterator.hasNext()) { // TODO: Confirm that `variablesForHashCode.map { it.name?.quoteIfNeeded()!! }` is safe here
|
||||
val validator = CollectingNameValidator(variablesForHashCode.map { it.name?.quoteIfNeeded()!! })
|
||||
val resultVarName = KotlinNameSuggester.suggestNameByName("result", validator)
|
||||
buildString {
|
||||
append("var $resultVarName = $initialValue\n")
|
||||
propertyIterator.forEach { append("$resultVarName = 31 * $resultVarName + ${it.genVariableHashCode(true)}\n") }
|
||||
append("return $resultVarName")
|
||||
}
|
||||
} else "return $initialValue"
|
||||
}
|
||||
return Pair(functionText, bodyText)
|
||||
}
|
||||
|
||||
context(KaSession)
|
||||
private fun findEqualsMethodForClass(classSymbol: KaClassSymbol): KaCallableSymbol? =
|
||||
findMethodInMemberScopeOrInAny(classSymbol, EQUALS) { matchesEqualsMethodSignature(it) }
|
||||
|
||||
context(KaSession)
|
||||
private fun findHashCodeMethodForClass(classSymbol: KaClassSymbol): KaCallableSymbol? =
|
||||
findMethodInMemberScopeOrInAny(classSymbol, HASH_CODE) { matchesHashCodeMethodSignature(it) }
|
||||
|
||||
context(KaSession)
|
||||
private fun findMethodInMemberScopeOrInAny(
|
||||
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 anySuperClassSymbol = classSymbol.superTypes.find { it.isAnyType }?.symbol as? KaClassSymbol ?: return null
|
||||
return anySuperClassSymbol.memberScope.callables(methodName).singleOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds methods whose name is [methodName] not only from the class [classSymbol] but also its parent classes,
|
||||
* and returns method symbols after filtering them using [condition].
|
||||
*/
|
||||
context(KaSession)
|
||||
private fun findMethod(
|
||||
classSymbol: KaClassSymbol, methodName: Name, condition: (KaCallableSymbol) -> Boolean
|
||||
): KaCallableSymbol? = classSymbol.memberScope.callables(methodName).filter(condition).singleOrNull()
|
||||
|
||||
/**
|
||||
* A function to generate the "not equals" comparison between the class of `this` and the class of the parameter.
|
||||
*/
|
||||
private fun generateClassLiteralsNotEqual(paramName: String, targetClass: KtClassOrObject): String {
|
||||
val defaultExpression = "javaClass != $paramName?.javaClass"
|
||||
if (!targetClass.languageVersionSettings.supportsFeature(LanguageFeature.BoundCallableReferences)) return defaultExpression
|
||||
return when {
|
||||
targetClass.platform.isCommon() -> "other == null || this::class != $paramName::class"
|
||||
targetClass.platform.isJs() -> "other == null || this::class.js != $paramName::class.js"
|
||||
else -> defaultExpression
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to generate the class i.e., `javaClass` or `this::class`.
|
||||
*/
|
||||
private fun generateClassLiteral(targetClass: KtClassOrObject): String {
|
||||
val defaultExpression = "javaClass"
|
||||
if (!targetClass.languageVersionSettings.supportsFeature(LanguageFeature.BoundCallableReferences)) return defaultExpression
|
||||
return when {
|
||||
targetClass.platform.isCommon() -> "this::class"
|
||||
targetClass.platform.isJs() -> "this::class.js"
|
||||
else -> defaultExpression
|
||||
}
|
||||
}
|
||||
|
||||
context(KaSession)
|
||||
private fun getPropertiesToUseInGeneratedMember(classOrObject: KtClassOrObject): List<KtNamedDeclaration> =
|
||||
buildList<KtNamedDeclaration> {
|
||||
classOrObject.primaryConstructorParameters.filterTo(this) { it.hasValOrVar() }
|
||||
classOrObject.declarations.asSequence().filterIsInstance<KtProperty>().filterTo(this) {
|
||||
it.symbol is KaPropertySymbol
|
||||
context(KaSession)
|
||||
private fun generateArrayHashCodeCall(
|
||||
variableType: KaType?, canUseContentFunctions: Boolean, argument: String
|
||||
): String {
|
||||
return if (canUseContentFunctions) {
|
||||
val methodName = if (variableType?.isNestedArray == true) "contentDeepHashCode" else "contentHashCode"
|
||||
val dot = if (variableType?.isMarkedNullable == true) "?." else "."
|
||||
"$argument$dot$methodName()"
|
||||
} else {
|
||||
val methodName = if (variableType?.isNestedArray == true) "deepHashCode" else "hashCode"
|
||||
"java.util.Arrays.$methodName($argument)"
|
||||
}
|
||||
}.filter {
|
||||
it.name?.quoteIfNeeded().isIdentifier()
|
||||
}
|
||||
|
||||
private fun KtElement.canUseArrayContentFunctions() = languageVersionSettings.apiVersion >= ApiVersion.KOTLIN_1_1
|
||||
|
||||
context(KaSession)
|
||||
private fun generateArraysEqualsCall(
|
||||
type: KaType, canUseContentFunctions: Boolean, arg1: String, arg2: String
|
||||
): String {
|
||||
return if (canUseContentFunctions) {
|
||||
val methodName = if (type.isNestedArray) "contentDeepEquals" else "contentEquals"
|
||||
"$arg1.$methodName($arg2)"
|
||||
} else {
|
||||
val methodName = if (type.isNestedArray) "deepEquals" else "equals"
|
||||
"java.util.Arrays.$methodName($arg1, $arg2)"
|
||||
}
|
||||
}
|
||||
|
||||
context(KaSession)
|
||||
private fun generateArrayHashCodeCall(
|
||||
variableType: KaType?, canUseContentFunctions: Boolean, argument: String
|
||||
): String {
|
||||
return if (canUseContentFunctions) {
|
||||
val methodName = if (variableType?.isNestedArray == true) "contentDeepHashCode" else "contentHashCode"
|
||||
val dot = if (variableType?.isMarkedNullable == true) "?." else "."
|
||||
"$argument$dot$methodName()"
|
||||
} else {
|
||||
val methodName = if (variableType?.isNestedArray == true) "deepHashCode" else "hashCode"
|
||||
"java.util.Arrays.$methodName($argument)"
|
||||
}
|
||||
}
|
||||
|
||||
+5
-8
@@ -10,10 +10,7 @@ import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
|
||||
import org.jetbrains.kotlin.idea.codeinsight.utils.DeletePsiElementsFix
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.generateEqualsHeaderAndBodyTexts
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.generateHashCodeHeaderAndBodyTexts
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.matchesEqualsMethodSignature
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.matchesHashCodeMethodSignature
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.GenerateEqualsAndHashCodeUtils
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.quickFix.GenerateEqualsFix
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.quickFix.GenerateHashCodeFix
|
||||
import org.jetbrains.kotlin.psi.KtClass
|
||||
@@ -32,11 +29,11 @@ internal class EqualsOrHashCodeInspection : AbstractKotlinInspection() {
|
||||
Pair(
|
||||
classOrObjectMemberDeclarations.singleOrNull {
|
||||
val function = it.symbol as? KaNamedFunctionSymbol ?: return@singleOrNull false
|
||||
matchesEqualsMethodSignature(function)
|
||||
GenerateEqualsAndHashCodeUtils.matchesEqualsMethodSignature(function)
|
||||
} as? KtNamedFunction,
|
||||
classOrObjectMemberDeclarations.singleOrNull {
|
||||
val function = it.symbol as? KaNamedFunctionSymbol ?: return@singleOrNull false
|
||||
matchesHashCodeMethodSignature(function)
|
||||
GenerateEqualsAndHashCodeUtils.matchesHashCodeMethodSignature(function)
|
||||
} as? KtNamedFunction,
|
||||
)
|
||||
}
|
||||
@@ -63,10 +60,10 @@ internal class EqualsOrHashCodeInspection : AbstractKotlinInspection() {
|
||||
)
|
||||
|
||||
val fix = if (equalsDeclaration != null) {
|
||||
val (function, body) = generateHashCodeHeaderAndBodyTexts(classOrObject)
|
||||
val (function, body) = GenerateEqualsAndHashCodeUtils.generateHashCodeHeaderAndBodyTexts(classOrObject)
|
||||
GenerateHashCodeFix(function, body)
|
||||
} else {
|
||||
val (function, body) = generateEqualsHeaderAndBodyTexts(classOrObject)
|
||||
val (function, body) = GenerateEqualsAndHashCodeUtils.generateEqualsHeaderAndBodyTexts(classOrObject)
|
||||
GenerateEqualsFix(function, body)
|
||||
}
|
||||
|
||||
|
||||
+5
-6
@@ -13,8 +13,7 @@ import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix
|
||||
import org.jetbrains.kotlin.idea.codeinsight.utils.isNullableAnyType
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.generateEqualsHeaderAndBodyTexts
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.generateHashCodeHeaderAndBodyTexts
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.intentions.GenerateEqualsAndHashCodeUtils
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.containingClass
|
||||
@@ -87,16 +86,16 @@ class ArrayInDataClassInspection : KotlinApplicableInspectionBase.Simple<KtParam
|
||||
return when (checkOverriddenEqualsAndHashCode(containingClass)) {
|
||||
EqualsHashCodeOverrides.HAS_EQUALS_AND_HASHCODE -> null
|
||||
EqualsHashCodeOverrides.HAS_EQUALS -> {
|
||||
val (hashCodeHeader, hashCodeBody) = generateHashCodeHeaderAndBodyTexts(containingClass)
|
||||
val (hashCodeHeader, hashCodeBody) = GenerateEqualsAndHashCodeUtils.generateHashCodeHeaderAndBodyTexts(containingClass)
|
||||
Context(equalsHeader = null, equalsBody = null, hashCodeHeader, hashCodeBody)
|
||||
}
|
||||
EqualsHashCodeOverrides.HAS_HASHCODE -> {
|
||||
val (equalsHeader, equalsBody) = generateEqualsHeaderAndBodyTexts(containingClass)
|
||||
val (equalsHeader, equalsBody) = GenerateEqualsAndHashCodeUtils.generateEqualsHeaderAndBodyTexts(containingClass)
|
||||
Context(equalsHeader, equalsBody, hashCodeHeader = null, hashCodeBody = null)
|
||||
}
|
||||
EqualsHashCodeOverrides.HAS_NONE -> {
|
||||
val (equalsHeader, equalsBody) = generateEqualsHeaderAndBodyTexts(containingClass)
|
||||
val (hashCodeHeader, hashCodeBody) = generateHashCodeHeaderAndBodyTexts(containingClass)
|
||||
val (equalsHeader, equalsBody) = GenerateEqualsAndHashCodeUtils.generateEqualsHeaderAndBodyTexts(containingClass)
|
||||
val (hashCodeHeader, hashCodeBody) = GenerateEqualsAndHashCodeUtils.generateHashCodeHeaderAndBodyTexts(containingClass)
|
||||
Context(equalsHeader, equalsBody, hashCodeHeader, hashCodeBody)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user