From e0c8142106898ecede7fd32e28ffd929c9056993 Mon Sep 17 00:00:00 2001 From: "vladislav.grechko" Date: Tue, 4 Oct 2022 20:22:11 +0200 Subject: [PATCH] Support of custom 'equals' and 'hashCode' in inline classes 'equals' from any made available for overriding in inline classes 'typed' equals made available for definition in inline classes 'typed' equals definition made compulsory if 'untyped' is overridden 'operator' keyword is allowed in 'typed' equals definition ^KT-24874: Fixed --- .../diagnostics/KtFirDataClassConverters.kt | 7 +++ .../api/fir/diagnostics/KtFirDiagnostics.kt | 5 ++ .../fir/diagnostics/KtFirDiagnosticsImpl.kt | 6 ++ ...CompilerTestFE10TestdataTestGenerated.java | 12 ++++ .../inlineClasses/inlineClassMembers.kt | 6 +- ...irOldFrontendDiagnosticsTestGenerated.java | 12 ++++ ...DiagnosticsWithLightTreeTestGenerated.java | 12 ++++ .../diagnostics/FirDiagnosticsList.kt | 4 ++ .../fir/analysis/diagnostics/FirErrors.kt | 1 + .../FirInlineClassDeclarationChecker.kt | 22 +++++++- .../declaration/FirOperatorModifierChecker.kt | 23 ++++++-- .../checkers/declaration/declarationUtils.kt | 25 +++++++++ .../diagnostics/FirErrorsDefaultMessages.kt | 7 +++ .../FirBlackBoxCodegenTestGenerated.java | 36 ++++++++++++ .../jetbrains/kotlin/diagnostics/Errors.java | 2 + .../rendering/DefaultErrorMessages.java | 3 + .../kotlin/resolve/OperatorModifierChecker.kt | 11 +++- .../checkers/InlineClassDeclarationChecker.kt | 23 +++++++- .../jvm/lower/JvmInlineClassLowering.kt | 51 ++++++++++++++--- .../ir/util/DataClassMembersGenerator.kt | 11 ++++ .../inlineClassBothEqualsOverride.kt | 55 +++++++++++++++++++ .../inlineClassEqualsConsistency.kt | 47 ++++++++++++++++ .../inlineClassEqualsOverride.kt | 35 ++++++++++++ ...nlineClassEqualsOverridenForCollections.kt | 22 ++++++++ .../inlineClassHashCodeOverride.kt | 14 +++++ .../inlineClassUntypedEqualsOverriden.kt | 23 ++++++++ .../inlineClassWithCustomEquals.kt | 3 - .../inlineClassWithCustomEqualsGeneric.kt | 3 - .../illegalEqualsOverridingInInlineClass.kt | 28 ++++++++++ .../illegalEqualsOverridingInInlineClass.txt | 26 +++++++++ ...mbersAndConstructsInsideInlineClass.fir.kt | 12 ++-- ...edMembersAndConstructsInsideInlineClass.kt | 12 ++-- ...ypedEqualsOperatorModifierInInlineClass.kt | 22 ++++++++ ...pedEqualsOperatorModifierInInlineClass.txt | 20 +++++++ ...embersAndConstructsInsideValueClass.fir.kt | 12 ++-- ...vedMembersAndConstructsInsideValueClass.kt | 12 ++-- .../test/runners/DiagnosticTestGenerated.java | 12 ++++ .../codegen/BlackBoxCodegenTestGenerated.java | 6 ++ .../IrBlackBoxCodegenTestGenerated.java | 36 ++++++++++++ .../LightAnalysisModeTestGenerated.java | 5 ++ .../kotlin/config/LanguageVersionSettings.kt | 2 + .../kotlin/descriptors/descriptorUtil.kt | 15 +++++ .../jetbrains/kotlin/util/modifierChecks.kt | 11 +++- .../js/test/JsCodegenBoxTestGenerated.java | 6 ++ .../test/ir/IrJsCodegenBoxTestGenerated.java | 6 ++ .../IrCodegenBoxWasmTestGenerated.java | 5 ++ .../NativeCodegenBoxTestGenerated.java | 8 +++ 47 files changed, 684 insertions(+), 53 deletions(-) create mode 100644 compiler/testData/codegen/box/inlineClasses/inlineClassBothEqualsOverride.kt create mode 100644 compiler/testData/codegen/box/inlineClasses/inlineClassEqualsConsistency.kt create mode 100644 compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverride.kt create mode 100644 compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverridenForCollections.kt create mode 100644 compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt create mode 100644 compiler/testData/codegen/box/inlineClasses/inlineClassUntypedEqualsOverriden.kt create mode 100644 compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt create mode 100644 compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.txt create mode 100644 compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt create mode 100644 compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.txt diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt index 5a6eecfba71..d89a6555cfc 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt @@ -3876,6 +3876,13 @@ internal val KT_DIAGNOSTIC_CONVERTER = KtDiagnosticConverterBuilder.buildConvert token, ) } + add(FirErrors.INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS) { firDiagnostic -> + InefficientEqualsOverridingInInlineClassImpl( + firDiagnostic.a, + firDiagnostic as KtPsiDiagnostic, + token, + ) + } add(FirErrors.CANNOT_ALL_UNDER_IMPORT_FROM_SINGLETON) { firDiagnostic -> CannotAllUnderImportFromSingletonImpl( firDiagnostic.a, diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt index 63ccb78d583..2e6ca203181 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt @@ -2704,6 +2704,11 @@ sealed class KtFirDiagnostic : KtDiagnosticWithPsi { override val diagnosticClass get() = RedundantInlineSuspendFunctionType::class } + abstract class InefficientEqualsOverridingInInlineClass : KtFirDiagnostic() { + override val diagnosticClass get() = InefficientEqualsOverridingInInlineClass::class + abstract val className: String + } + abstract class CannotAllUnderImportFromSingleton : KtFirDiagnostic() { override val diagnosticClass get() = CannotAllUnderImportFromSingleton::class abstract val objectName: Name diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt index 62fcd7f518a..ee48eedcbae 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt @@ -3261,6 +3261,12 @@ internal class RedundantInlineSuspendFunctionTypeImpl( override val token: KtLifetimeToken, ) : KtFirDiagnostic.RedundantInlineSuspendFunctionType(), KtAbstractFirDiagnostic +internal class InefficientEqualsOverridingInInlineClassImpl( + override val className: String, + override val firDiagnostic: KtPsiDiagnostic, + override val token: KtLifetimeToken, +) : KtFirDiagnostic.InefficientEqualsOverridingInInlineClass(), KtAbstractFirDiagnostic + internal class CannotAllUnderImportFromSingletonImpl( override val objectName: Name, override val firDiagnostic: KtPsiDiagnostic, diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java index b3a70e2968d..bc8ac6a80b6 100644 --- a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java @@ -17854,6 +17854,12 @@ public class DiagnosisCompilerTestFE10TestdataTestGenerated extends AbstractDiag runTest("compiler/testData/diagnostics/tests/inlineClasses/identityComparisonWithInlineClasses.kt"); } + @Test + @TestMetadata("illegalEqualsOverridingInInlineClass.kt") + public void testIllegalEqualsOverridingInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt"); + } + @Test @TestMetadata("inlineClassCanImplementInterfaceByDelegation.kt") public void testInlineClassCanImplementInterfaceByDelegation() throws Exception { @@ -17962,6 +17968,12 @@ public class DiagnosisCompilerTestFE10TestdataTestGenerated extends AbstractDiag runTest("compiler/testData/diagnostics/tests/inlineClasses/synchronizedForbidden.kt"); } + @Test + @TestMetadata("typedEqualsOperatorModifierInInlineClass.kt") + public void testTypedEqualsOperatorModifierInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt"); + } + @Test @TestMetadata("unsignedLiteralsWithoutArtifactOnClasspath.kt") public void testUnsignedLiteralsWithoutArtifactOnClasspath() throws Exception { diff --git a/compiler/fir/analysis-tests/testData/resolve/inlineClasses/inlineClassMembers.kt b/compiler/fir/analysis-tests/testData/resolve/inlineClasses/inlineClassMembers.kt index f43088773c3..5a7018fee20 100644 --- a/compiler/fir/analysis-tests/testData/resolve/inlineClasses/inlineClassMembers.kt +++ b/compiler/fir/analysis-tests/testData/resolve/inlineClasses/inlineClassMembers.kt @@ -1,3 +1,5 @@ +// !LANGUAGE: +CustomEqualsInInlineClasses + value class BackingFields(val x: Int) { val y = 0 var z: String @@ -16,8 +18,8 @@ inline class ReversedMembers(val x: Int) { fun box() {} fun unbox() {} - override fun equals(other: Any?) = true - override fun hashCode() = 1 + override fun equals(other: Any?) = true + override fun hashCode() = 1 } inline class SecondaryConstructors(val x: Int) { diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java index d3bc51441c7..bdc917ffd8c 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java @@ -17854,6 +17854,12 @@ public class FirOldFrontendDiagnosticsTestGenerated extends AbstractFirDiagnosti runTest("compiler/testData/diagnostics/tests/inlineClasses/identityComparisonWithInlineClasses.kt"); } + @Test + @TestMetadata("illegalEqualsOverridingInInlineClass.kt") + public void testIllegalEqualsOverridingInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt"); + } + @Test @TestMetadata("inlineClassCanImplementInterfaceByDelegation.kt") public void testInlineClassCanImplementInterfaceByDelegation() throws Exception { @@ -17962,6 +17968,12 @@ public class FirOldFrontendDiagnosticsTestGenerated extends AbstractFirDiagnosti runTest("compiler/testData/diagnostics/tests/inlineClasses/synchronizedForbidden.kt"); } + @Test + @TestMetadata("typedEqualsOperatorModifierInInlineClass.kt") + public void testTypedEqualsOperatorModifierInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt"); + } + @Test @TestMetadata("unsignedLiteralsWithoutArtifactOnClasspath.kt") public void testUnsignedLiteralsWithoutArtifactOnClasspath() throws Exception { diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java index 2409ee23ea2..02518e0cdce 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java @@ -17854,6 +17854,12 @@ public class FirOldFrontendDiagnosticsWithLightTreeTestGenerated extends Abstrac runTest("compiler/testData/diagnostics/tests/inlineClasses/identityComparisonWithInlineClasses.kt"); } + @Test + @TestMetadata("illegalEqualsOverridingInInlineClass.kt") + public void testIllegalEqualsOverridingInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt"); + } + @Test @TestMetadata("inlineClassCanImplementInterfaceByDelegation.kt") public void testInlineClassCanImplementInterfaceByDelegation() throws Exception { @@ -17962,6 +17968,12 @@ public class FirOldFrontendDiagnosticsWithLightTreeTestGenerated extends Abstrac runTest("compiler/testData/diagnostics/tests/inlineClasses/synchronizedForbidden.kt"); } + @Test + @TestMetadata("typedEqualsOperatorModifierInInlineClass.kt") + public void testTypedEqualsOperatorModifierInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt"); + } + @Test @TestMetadata("unsignedLiteralsWithoutArtifactOnClasspath.kt") public void testUnsignedLiteralsWithoutArtifactOnClasspath() throws Exception { diff --git a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirDiagnosticsList.kt b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirDiagnosticsList.kt index 3ea2a8b8394..38791a7b720 100644 --- a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirDiagnosticsList.kt +++ b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirDiagnosticsList.kt @@ -1397,6 +1397,10 @@ object DIAGNOSTICS_LIST : DiagnosticList("FirErrors") { val INLINE_SUSPEND_FUNCTION_TYPE_UNSUPPORTED by error() val REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE by warning(PositioningStrategy.SUSPEND_MODIFIER) + + val INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS by warning(PositioningStrategy.DECLARATION_SIGNATURE) { + parameter("className") + } } val IMPORTS by object : DiagnosticGroup("Imports") { diff --git a/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt b/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt index e684c1971a3..4177ac9e146 100644 --- a/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt +++ b/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt @@ -723,6 +723,7 @@ object FirErrors { val ILLEGAL_INLINE_PARAMETER_MODIFIER by error0(SourceElementPositioningStrategies.INLINE_PARAMETER_MODIFIER) val INLINE_SUSPEND_FUNCTION_TYPE_UNSUPPORTED by error0() val REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE by warning0(SourceElementPositioningStrategies.SUSPEND_MODIFIER) + val INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS by warning1(SourceElementPositioningStrategies.DECLARATION_SIGNATURE) // Imports val CANNOT_ALL_UNDER_IMPORT_FROM_SINGLETON by error1(SourceElementPositioningStrategies.IMPORT_LAST_NAME) diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirInlineClassDeclarationChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirInlineClassDeclarationChecker.kt index 2d4f8884822..42e99189851 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirInlineClassDeclarationChecker.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirInlineClassDeclarationChecker.kt @@ -32,7 +32,8 @@ import org.jetbrains.kotlin.name.StandardClassIds object FirInlineClassDeclarationChecker : FirRegularClassChecker() { - private val reservedFunctionNames = setOf("box", "unbox", "equals", "hashCode") + private val boxAndUnboxNames = setOf("box", "unbox") + private val equalsAndHashCodeNames = setOf("equals", "hashCode") private val javaLangFqName = FqName("java.lang") private val cloneableFqName = FqName("Cloneable") @@ -92,7 +93,10 @@ object FirInlineClassDeclarationChecker : FirRegularClassChecker() { is FirSimpleFunction -> { val functionName = innerDeclaration.name.asString() - if (functionName in reservedFunctionNames) { + if (functionName in boxAndUnboxNames + || (functionName in equalsAndHashCodeNames + && !context.languageVersionSettings.supportsFeature(LanguageFeature.CustomEqualsInInlineClasses)) + ) { reporter.reportOn( innerDeclaration.source, FirErrors.RESERVED_MEMBER_INSIDE_VALUE_CLASS, functionName, context ) @@ -193,6 +197,20 @@ object FirInlineClassDeclarationChecker : FirRegularClassChecker() { } } } + + if (context.languageVersionSettings.supportsFeature(LanguageFeature.CustomEqualsInInlineClasses)) { + val simpleFunctions = declaration.declarations.filterIsInstance() + simpleFunctions.singleOrNull() { it.overridesEqualsFromAny() }?.apply { + if (declaration.symbol.isInline && simpleFunctions.none { it.isTypedEqualsInInlineClass(context.session) }) { + reporter.reportOn( + source, + FirErrors.INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS, + declaration.name.asString(), + context + ) + } + } + } } private fun FirProperty.isRelatedToParameter(parameter: FirValueParameter?) = diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirOperatorModifierChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirOperatorModifierChecker.kt index 5ee2872da10..e5d44566d63 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirOperatorModifierChecker.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirOperatorModifierChecker.kt @@ -10,6 +10,7 @@ package org.jetbrains.kotlin.fir.analysis.checkers.declaration +import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.declaration.Checks.Returns import org.jetbrains.kotlin.fir.analysis.checkers.declaration.Checks.ValueParametersCount @@ -25,6 +26,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors import org.jetbrains.kotlin.diagnostics.reportOn import org.jetbrains.kotlin.fir.containingClass import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction +import org.jetbrains.kotlin.fir.declarations.utils.isInline import org.jetbrains.kotlin.fir.declarations.utils.isOperator import org.jetbrains.kotlin.fir.resolve.toFirRegularClassSymbol import org.jetbrains.kotlin.fir.symbols.impl.ConeClassLikeLookupTagImpl @@ -185,10 +187,23 @@ private object OperatorFunctionChecks { checkFor( EQUALS, member, - Checks.full("must override ''equals()'' in Any") { ctx, function -> - val containingClassSymbol = function.containingClass()?.toFirRegularClassSymbol(ctx.session) ?: return@full true - function.overriddenFunctions(containingClassSymbol, ctx).any { - it.containingClass()?.classId == StandardClassIds.Any + object : Check { + override fun check(context: CheckerContext, function: FirSimpleFunction): String? { + val containingClassSymbol = function.containingClass()?.toFirRegularClassSymbol(context.session) ?: return null + val customEqualsSupported = context.languageVersionSettings.supportsFeature(LanguageFeature.CustomEqualsInInlineClasses) + + if (function.overriddenFunctions(containingClassSymbol, context) + .any { it.containingClass()?.classId == StandardClassIds.Any } + || (customEqualsSupported && function.isTypedEqualsInInlineClass(context.session)) + ) { + return null + } + return buildString { + append("must override ''equals()'' in Any") + if (customEqualsSupported && containingClassSymbol.isInline) { + append(" or define ''equals(other: ${containingClassSymbol.name}): Boolean''") + } + } } } ) diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/declarationUtils.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/declarationUtils.kt index b09dd153db5..73662d274aa 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/declarationUtils.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/declarationUtils.kt @@ -7,15 +7,23 @@ package org.jetbrains.kotlin.fir.analysis.checkers.declaration import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.fir.FirSession import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.hasModifier import org.jetbrains.kotlin.fir.analysis.checkers.modality +import org.jetbrains.kotlin.fir.containingClass import org.jetbrains.kotlin.fir.containingClassForStaticMemberAttr import org.jetbrains.kotlin.fir.declarations.* import org.jetbrains.kotlin.fir.declarations.utils.* +import org.jetbrains.kotlin.fir.resolve.toFirRegularClassSymbol import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.impl.* +import org.jetbrains.kotlin.fir.types.classId +import org.jetbrains.kotlin.fir.types.coneType +import org.jetbrains.kotlin.fir.types.isBoolean +import org.jetbrains.kotlin.fir.types.isNullableAny import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.util.OperatorNameConventions internal fun isInsideExpectClass(containingClass: FirClass, context: CheckerContext): Boolean { return isInsideSpecificClass(containingClass, context) { klass -> klass is FirRegularClass && klass.isExpect } @@ -110,3 +118,20 @@ fun FirClassSymbol<*>.primaryConstructorSymbol(): FirConstructorSymbol? { } return null } + +fun FirSimpleFunction.isTypedEqualsInInlineClass(session: FirSession): Boolean = + containingClass()?.toFirRegularClassSymbol(session)?.run { + this@isTypedEqualsInInlineClass.contextReceivers.isEmpty() + && this@isTypedEqualsInInlineClass.receiverTypeRef == null + && this@isTypedEqualsInInlineClass.name == OperatorNameConventions.EQUALS + && this@isTypedEqualsInInlineClass.returnTypeRef.isBoolean + && isInline + && this@isTypedEqualsInInlineClass.valueParameters.size == 1 + && this@isTypedEqualsInInlineClass.valueParameters[0].returnTypeRef.coneType.classId == classId + } ?: false + +fun FirSimpleFunction.overridesEqualsFromAny(): Boolean { + return name == OperatorNameConventions.EQUALS && returnTypeRef.isBoolean + && valueParameters.size == 1 && valueParameters[0].returnTypeRef.isNullableAny + && contextReceivers.isEmpty() && receiverTypeRef == null +} \ No newline at end of file diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrorsDefaultMessages.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrorsDefaultMessages.kt index 00db56a62d9..54e2219e393 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrorsDefaultMessages.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrorsDefaultMessages.kt @@ -134,6 +134,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.CYCLIC_CONSTRUCTO import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.CYCLIC_GENERIC_UPPER_BOUND import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.CYCLIC_INHERITANCE_HIERARCHY import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.DANGEROUS_CHARACTERS +import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.DATA_CLASS_NOT_PROPERTY_PARAMETER import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.DATA_CLASS_OVERRIDE_CONFLICT import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.DATA_CLASS_VARARG_PARAMETER @@ -2003,6 +2004,12 @@ object FirErrorsDefaultMessages : BaseDiagnosticRendererFactory() { "Redundant 'suspend' modifier: lambda parameters of suspend function type uses existing continuation." ) + map.put( + INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS, + "Overriding ''equals'' from ''Any'' in inline class alongside with lack of ''equals(other: {0}): Boolean'' leads to boxing on every equality comparison", + STRING + ) + //imports map.put( CANNOT_ALL_UNDER_IMPORT_FROM_SINGLETON, diff --git a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java index b2b922e9533..1bafb39c8fb 100644 --- a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java +++ b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java @@ -21353,6 +21353,12 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT runTest("compiler/testData/codegen/box/inlineClasses/inlineClassAsLastExpressionInInLambdaGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassBothEqualsOverride.kt") + public void testInlineClassBothEqualsOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassBothEqualsOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassEqualityShouldUseTotalOrderForFloatingPointData.kt") public void testInlineClassEqualityShouldUseTotalOrderForFloatingPointData() throws Exception { @@ -21365,6 +21371,24 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualityShouldUseTotalOrderForFloatingPointDataGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassEqualsConsistency.kt") + public void testInlineClassEqualsConsistency() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualsConsistency.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + + @Test + @TestMetadata("inlineClassEqualsOverride.kt") + public void testInlineClassEqualsOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + + @Test + @TestMetadata("inlineClassEqualsOverridenForCollections.kt") + public void testInlineClassEqualsOverridenForCollections() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverridenForCollections.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassFieldHandling.kt") public void testInlineClassFieldHandling() throws Exception { @@ -21389,6 +21413,12 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassImplementsCollection.kt") public void testInlineClassImplementsCollection() throws Exception { @@ -21437,6 +21467,12 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT runTest("compiler/testData/codegen/box/inlineClasses/inlineClassPropertyReferenceGetAndSetGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassUntypedEqualsOverriden.kt") + public void testInlineClassUntypedEqualsOverriden() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassUntypedEqualsOverriden.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassValueCapturedInInlineLambda.kt") public void testInlineClassValueCapturedInInlineLambda() throws Exception { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java index b5e975173b6..1342545f78c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java @@ -428,6 +428,8 @@ public interface Errors { DiagnosticFactory0 VALUE_CLASS_CANNOT_BE_CLONEABLE = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 INLINE_CLASS_DEPRECATED = DiagnosticFactory0.create(WARNING); DiagnosticFactory0 INLINE_CLASS_CANNOT_HAVE_CONTEXT_RECEIVERS = DiagnosticFactory0.create(ERROR); + DiagnosticFactory1 INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS = + DiagnosticFactory1.create(WARNING, DECLARATION_SIGNATURE); // Result class diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java index 7e53946388f..853e2608232 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java @@ -787,6 +787,9 @@ public class DefaultErrorMessages { MAP.put(VALUE_CLASS_CANNOT_BE_CLONEABLE, "Value class cannot be Cloneable"); MAP.put(INLINE_CLASS_DEPRECATED, "'inline' modifier is deprecated. Use 'value' instead"); MAP.put(INLINE_CLASS_CANNOT_HAVE_CONTEXT_RECEIVERS, "Inline classes cannot have context receivers"); + MAP.put(INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS, + "Overriding ''equals'' from ''Any'' in inline class alongside with lack of ''equals(other: {0}): Boolean'' leads to boxing on every equality comparison", + STRING); MAP.put(RESULT_CLASS_IN_RETURN_TYPE, "'kotlin.Result' cannot be used as a return type"); MAP.put(RESULT_CLASS_WITH_NULLABLE_OPERATOR, "Expression of type ''kotlin.Result'' cannot be used as a left operand of ''{0}''", STRING); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/OperatorModifierChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/OperatorModifierChecker.kt index be9f638df24..09949d1a411 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/OperatorModifierChecker.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/OperatorModifierChecker.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.config.LanguageVersionSettings import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.isTypedEqualsInInlineClass import org.jetbrains.kotlin.diagnostics.DiagnosticSink import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.lexer.KtTokens @@ -44,11 +45,15 @@ object OperatorModifierChecker { val checkResult = OperatorChecks.check(functionDescriptor) if (checkResult.isSuccess) { - when (functionDescriptor.name) { - in REM_TO_MOD_OPERATION_NAMES.keys -> + when { + functionDescriptor.name in REM_TO_MOD_OPERATION_NAMES.keys -> checkSupportsFeature(LanguageFeature.OperatorRem, languageVersionSettings, diagnosticHolder, modifier) - OperatorNameConventions.PROVIDE_DELEGATE -> + + functionDescriptor.name == OperatorNameConventions.PROVIDE_DELEGATE -> checkSupportsFeature(LanguageFeature.OperatorProvideDelegate, languageVersionSettings, diagnosticHolder, modifier) + + functionDescriptor.isTypedEqualsInInlineClass() -> + checkSupportsFeature(LanguageFeature.CustomEqualsInInlineClasses, languageVersionSettings, diagnosticHolder, modifier) } if (functionDescriptor.name in REM_TO_MOD_OPERATION_NAMES.values && diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/InlineClassDeclarationChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/InlineClassDeclarationChecker.kt index 4a4cd28a1bd..57831190535 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/InlineClassDeclarationChecker.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/InlineClassDeclarationChecker.kt @@ -161,6 +161,21 @@ object InlineClassDeclarationChecker : DeclarationChecker { trace.report(Errors.VALUE_CLASS_CANNOT_BE_CLONEABLE.on(inlineOrValueKeyword)) return } + + fun getFunctionDescriptor(declaration: KtNamedFunction): SimpleFunctionDescriptor? = + context.trace.bindingContext.get(BindingContext.FUNCTION, declaration) + + fun isUntypedEquals(declaration: KtNamedFunction): Boolean = getFunctionDescriptor(declaration)?.overridesEqualsFromAny() ?: false + fun isTypedEquals(declaration: KtNamedFunction): Boolean = getFunctionDescriptor(declaration)?.isTypedEqualsInInlineClass() ?: false + fun KtClass.namedFunctions() = declarations.filterIsInstance() + + if (context.languageVersionSettings.supportsFeature(LanguageFeature.CustomEqualsInInlineClasses)) { + declaration.namedFunctions().singleOrNull { isUntypedEquals(it) }?.apply { + if (declaration.namedFunctions().none { isTypedEquals(it) }) { + trace.report(Errors.INEFFICIENT_EQUALS_OVERRIDING_IN_INLINE_CLASS.on(this@apply, descriptor.name.asString())) + } + } + } } private fun KotlinType.isInapplicableParameterType() = @@ -213,7 +228,8 @@ class InnerClassInsideInlineClass : DeclarationChecker { class ReservedMembersAndConstructsForInlineClass : DeclarationChecker { companion object { - private val reservedFunctions = setOf("box", "unbox", "equals", "hashCode") + private val boxAndUnboxNames = setOf("box", "unbox") + private val equalsAndHashCodeNames = setOf("equals", "hashCode") } override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) { @@ -226,7 +242,10 @@ class ReservedMembersAndConstructsForInlineClass : DeclarationChecker { is SimpleFunctionDescriptor -> { val ktFunction = declaration as? KtFunction ?: return val functionName = descriptor.name.asString() - if (functionName in reservedFunctions) { + if (functionName in boxAndUnboxNames + || (functionName in equalsAndHashCodeNames + && !context.languageVersionSettings.supportsFeature(LanguageFeature.CustomEqualsInInlineClasses)) + ) { val nameIdentifier = ktFunction.nameIdentifier ?: return context.trace.report(Errors.RESERVED_MEMBER_INSIDE_VALUE_CLASS.on(nameIdentifier, functionName)) } diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt index 78552284b1d..b7f447af268 100644 --- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt @@ -24,14 +24,12 @@ import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.* import org.jetbrains.kotlin.ir.symbols.IrValueSymbol import org.jetbrains.kotlin.ir.transformStatement -import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.classOrNull -import org.jetbrains.kotlin.ir.types.isNullable -import org.jetbrains.kotlin.ir.types.makeNotNull +import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.JVM_INLINE_ANNOTATION_FQ_NAME +import org.jetbrains.kotlin.util.OperatorNameConventions.EQUALS val jvmInlineClassPhase = makeIrFilePhase( ::JvmInlineClassLowering, @@ -487,15 +485,52 @@ private class JvmInlineClassLowering(context: JvmBackendContext) : JvmValueClass val right = function.valueParameters[1] val type = left.type.unboxInlineClass() + val typedEqualsStaticReplacement = findStaticReplacementForTypedEquals(valueClass) + val untypedEquals = valueClass.functions.single { overridesEqualsFromAny(it) } + function.body = context.createIrBuilder(valueClass.symbol).run { irExprBody( - irEquals( - coerceInlineClasses(irGet(left), left.type, type), - coerceInlineClasses(irGet(right), right.type, type) - ) + if (typedEqualsStaticReplacement == null) { + if (untypedEquals.origin == IrDeclarationOrigin.DEFINED) { + val boxFunction = this@JvmInlineClassLowering.context.inlineClassReplacements.getBoxFunction(valueClass) + + fun irBox(expr: IrExpression) = irCall(boxFunction).apply { putValueArgument(0, expr) } + + irCall(untypedEquals).apply { + dispatchReceiver = irBox(irGet(left)) + putValueArgument(0, irBox(irGet(right))) + } + } else { + irEquals(coerceInlineClasses(irGet(left), left.type, type), coerceInlineClasses(irGet(right), right.type, type)) + } + } else { + irCall(typedEqualsStaticReplacement).apply { + putValueArgument(0, irGet(left)) + putValueArgument(1, irGet(right)) + } + } ) } valueClass.declarations += function } + + private fun overridesEqualsFromAny(irFunction: IrFunction): Boolean { + return irFunction.run { + name == EQUALS && returnType.isBoolean() && valueParameters.size == 1 + && valueParameters[0].type.isNullableAny() && contextReceiverParametersCount == 0 && extensionReceiverParameter == null + } + } + + private fun findStaticReplacementForTypedEquals(valueClass: IrClass): IrFunction? { + fun isTypedEquals(irFunction: IrFunction): Boolean { + return irFunction.run { + name == EQUALS && returnType.isBoolean() && valueParameters.size == 1 + && (valueParameters[0].type.classFqName?.run { valueClass.hasEqualFqName(this) } ?: false) + && contextReceiverParametersCount == 0 && extensionReceiverParameter == null + } + } + return valueClass.functions + .singleOrNull { context.inlineClassReplacements.originalFunctionForStaticReplacement[it]?.run { isTypedEquals(this) } ?: false } + } } diff --git a/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/DataClassMembersGenerator.kt b/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/DataClassMembersGenerator.kt index 9b5714b54bf..31fb2c3fc34 100644 --- a/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/DataClassMembersGenerator.kt +++ b/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/DataClassMembersGenerator.kt @@ -128,6 +128,17 @@ abstract class DataClassMembersGenerator( fun generateEqualsMethodBody(properties: List) { val irType = irClass.defaultType + val typedEqualsFunction = irClass.functions.singleOrNull { it.descriptor.isTypedEqualsInInlineClass() } + if (irClass.isSingleFieldValueClass && typedEqualsFunction != null) { + +irIfThenReturnFalse(irNotIs(irOther(), irType)) + val otherCasted = irImplicitCast(irOther(), irType) + +irReturn(irCall(typedEqualsFunction).apply { + putArgument(typedEqualsFunction.dispatchReceiverParameter!!, irThis()) + putValueArgument(0, otherCasted) + }) + return + } + if (!irClass.isValue) { +irIfThenReturnTrue(irEqeqeq(irThis(), irOther())) } diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassBothEqualsOverride.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassBothEqualsOverride.kt new file mode 100644 index 00000000000..b1f04142e48 --- /dev/null +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassBothEqualsOverride.kt @@ -0,0 +1,55 @@ +// WITH_STDLIB +// WORKS_WHEN_VALUE_CLASS +// LANGUAGE: +ValueClasses, +CustomEqualsInInlineClasses +// TARGET_BACKEND: JVM_IR + + +interface I { + fun getVal(): Int +} + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC1(val x: Int) : I { + override fun getVal(): Int { + return x + } + + fun equals(other: IC1): Boolean { + return x == other.x + } + + override fun equals(other: Any?): Boolean { + if (other !is I) { + return false + } + return getVal() == other.getVal() + } + + override fun hashCode(): Int { + return getVal() + } +} + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC2(val y: Int) : I { + override fun getVal(): Int { + return y * 10 + } + + fun equals(other: IC2): Boolean { + return y == other.y + } + + override fun equals(other: Any?): Boolean { + if (other !is I) { + return false + } + return getVal() == other.getVal() + } + + override fun hashCode(): Int { + return getVal() + } +} + +fun box(): String = if (setOf(IC1(10), IC2(1)).size == 1) "OK" else "Fail" \ No newline at end of file diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsConsistency.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsConsistency.kt new file mode 100644 index 00000000000..0e707411ef3 --- /dev/null +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsConsistency.kt @@ -0,0 +1,47 @@ +// WITH_STDLIB +// WORKS_WHEN_VALUE_CLASS +// LANGUAGE: +ValueClasses, +CustomEqualsInInlineClasses +// TARGET_BACKEND: JVM_IR + +import java.lang.AssertionError +import kotlin.math.abs + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC1(val x: Double) { + fun equals(other: IC1): Boolean { + return abs(x - other.x) < 0.5 + } +} + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC2(val x: Int) { + override fun equals(other: Any?): Boolean { + if (other !is IC2) { + return false + } + return abs(x - other.x) < 2 + } +} + +fun box(): String { + val a1Typed: IC1 = IC1(1.0) + val b1Typed: IC1 = IC1(1.1) + val c1Typed: IC1 = IC1(5.0) + val a1Untyped: Any = a1Typed + val b1Untyped: Any = b1Typed + val c1Untyped: Any = c1Typed + + val a2Typed: IC2 = IC2(1) + val b2Typed: IC2 = IC2(2) + val c2Typed: IC2 = IC2(5) + val a2Untyped: Any = a2Typed + val b2Untyped: Any = b2Typed + val c2Untyped: Any = c2Typed + + if ((a1Typed == b1Typed) != (a1Untyped == b1Untyped)) throw AssertionError() + if ((a1Typed == c1Typed) != (a1Untyped == c1Untyped)) throw AssertionError() + if ((a2Typed == b2Typed) != (a2Untyped == b2Untyped)) throw AssertionError() + if ((a2Typed == c2Typed) != (a2Untyped == c2Untyped)) throw AssertionError() + + return "OK" +} \ No newline at end of file diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverride.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverride.kt new file mode 100644 index 00000000000..f582f9d070e --- /dev/null +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverride.kt @@ -0,0 +1,35 @@ +// WITH_STDLIB +// WORKS_WHEN_VALUE_CLASS +// LANGUAGE: +ValueClasses, +CustomEqualsInInlineClasses +// TARGET_BACKEND: JVM_IR + +import kotlin.math.abs + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC1(val value: Double) { + fun equals(other: IC1): Boolean { + return abs(value - other.value) < 0.1 + } +} + +interface I { + fun equals(param: IC2): Boolean +} + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC2(val value: Int) : I { + override fun equals(param: IC2): Boolean { + return abs(value - param.value) < 2 + } +} + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC3(val value: Int) { + +} + +fun box() = + if (IC1(1.0) == IC1(1.05) && IC1(1.0) != IC1(1.2) + && IC2(5) == IC2(6) && IC2(5) != IC2(7) + && IC3(5) == IC3(5) && IC3(5) != IC3(6) + && IC1(1.0) != Any()) "OK" else "Fail" \ No newline at end of file diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverridenForCollections.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverridenForCollections.kt new file mode 100644 index 00000000000..4a630ad9734 --- /dev/null +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverridenForCollections.kt @@ -0,0 +1,22 @@ +// WITH_STDLIB +// WORKS_WHEN_VALUE_CLASS +// LANGUAGE: +ValueClasses, +CustomEqualsInInlineClasses +// TARGET_BACKEND: JVM_IR + +import kotlin.math.abs + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC(val x: Double) { + fun equals(other: IC): Boolean { + return abs(x - other.x) < 0.1 + } + + override fun hashCode(): Int { + return 0 + } +} + +fun box(): String { + val set = setOf(IC(1.0), IC(1.5), IC(1.501)) + return if (set.size == 2) "OK" else "Fail" +} \ No newline at end of file diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt new file mode 100644 index 00000000000..01e5313bf3d --- /dev/null +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt @@ -0,0 +1,14 @@ +// WITH_STDLIB +// WORKS_WHEN_VALUE_CLASS +// LANGUAGE: +ValueClasses, +CustomEqualsInInlineClasses + +OPTIONAL_JVM_INLINE_ANNOTATION +value class A(val value: MyClass) { + override fun hashCode() = 42 +} + +class MyClass() { + override fun hashCode() = -1 +} + +fun box(): String = if (A(MyClass()).hashCode() == 42) "OK" else "Fail" \ No newline at end of file diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassUntypedEqualsOverriden.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassUntypedEqualsOverriden.kt new file mode 100644 index 00000000000..dc509cb2958 --- /dev/null +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassUntypedEqualsOverriden.kt @@ -0,0 +1,23 @@ +// WITH_STDLIB +// WORKS_WHEN_VALUE_CLASS +// LANGUAGE: +ValueClasses, +CustomEqualsInInlineClasses +// TARGET_BACKEND: JVM_IR + +import kotlin.math.abs + +OPTIONAL_JVM_INLINE_ANNOTATION +value class IC(val x: Int) { + override fun equals(other: Any?): Boolean { + if (other !is IC) { + return false + } + return abs(x - other.x) < 2 + } + + override fun hashCode() = 0 +} + +fun box(): String { + val set = setOf(IC(1), IC(2), IC(5)) + return if (set.size == 2 && IC(1) == IC(1) && IC(1) == IC(2) && IC(1) != IC(5)) "OK" else "Fail" +} \ No newline at end of file diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEquals.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEquals.kt index e9c4b583136..82d583f0b61 100644 --- a/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEquals.kt +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEquals.kt @@ -1,7 +1,4 @@ -// IGNORE_BACKEND_FIR: JVM_IR -// FIR status: not supported in JVM // IGNORE_BACKEND: JVM -// IGNORE_BACKEND: JVM_IR // WITH_STDLIB // WORKS_WHEN_VALUE_CLASS // LANGUAGE: +ValueClasses diff --git a/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEqualsGeneric.kt b/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEqualsGeneric.kt index 6d0c50976d4..18d05a021d1 100644 --- a/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEqualsGeneric.kt +++ b/compiler/testData/codegen/box/inlineClasses/inlineClassWithCustomEqualsGeneric.kt @@ -1,7 +1,4 @@ -// IGNORE_BACKEND_FIR: JVM_IR -// FIR status: not supported in JVM // IGNORE_BACKEND: JVM -// IGNORE_BACKEND: JVM_IR // WITH_STDLIB // WORKS_WHEN_VALUE_CLASS // LANGUAGE: +ValueClasses, +GenericInlineClassParameter diff --git a/compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt b/compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt new file mode 100644 index 00000000000..da15ce1074e --- /dev/null +++ b/compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt @@ -0,0 +1,28 @@ +// FIR_IDENTICAL +// WITH_STDLIB +// !DIAGNOSTICS: -DEBUG_INFO_SMARTCAST +// LANGUAGE: +CustomEqualsInInlineClasses + +@JvmInline +value class IC1(val x: Int) { + override fun equals(other: Any?): Boolean { + if (other !is IC1) { + return false + } + return x == other.x + } +} + +@JvmInline +value class IC2(val x: Int) { + override fun hashCode() = 0 +} + +@JvmInline +value class IC3(val x: Int) { + override fun equals(other: Any?) = true + + fun equals(other: IC3) = true + + override fun hashCode() = 0 +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.txt b/compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.txt new file mode 100644 index 00000000000..06e3b9dcd95 --- /dev/null +++ b/compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.txt @@ -0,0 +1,26 @@ +package + +@kotlin.jvm.JvmInline public final value class IC1 { + public constructor IC1(/*0*/ x: kotlin.Int) + public final val x: kotlin.Int + public open override /*1*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*synthesized*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*synthesized*/ fun toString(): kotlin.String +} + +@kotlin.jvm.JvmInline public final value class IC2 { + public constructor IC2(/*0*/ x: kotlin.Int) + public final val x: kotlin.Int + public open override /*1*/ /*synthesized*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*synthesized*/ fun toString(): kotlin.String +} + +@kotlin.jvm.JvmInline public final value class IC3 { + public constructor IC3(/*0*/ x: kotlin.Int) + public final val x: kotlin.Int + public final fun equals(/*0*/ other: IC3): kotlin.Boolean + public open override /*1*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*synthesized*/ fun toString(): kotlin.String +} diff --git a/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.fir.kt b/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.fir.kt index a03875eb0de..e439297e146 100644 --- a/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.fir.kt +++ b/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.fir.kt @@ -1,4 +1,4 @@ -// !LANGUAGE: +InlineClasses, -JvmInlineValueClasses +// !LANGUAGE: +InlineClasses, -JvmInlineValueClasses, +CustomEqualsInInlineClasses // !DIAGNOSTICS: -UNUSED_PARAMETER inline class IC1(val x: Any) { @@ -8,8 +8,8 @@ inline class IC1(val x: Any) { fun unbox() {} fun unbox(x: Any) {} - override fun equals(other: Any?): Boolean = true - override fun hashCode(): Int = 0 + override fun equals(other: Any?): Boolean = true + override fun hashCode(): Int = 0 } inline class IC2(val x: Any) { @@ -19,15 +19,15 @@ inline class IC2(val x: Any) { fun unbox(x: Any) {} fun unbox(): Any = TODO() - fun equals(my: Any, other: Any): Boolean = true - fun hashCode(a: Any): Int = 0 + fun equals(my: Any, other: Any): Boolean = true + fun hashCode(a: Any): Int = 0 } inline class IC3(val x: Any) { fun box(x: Any): Any = TODO() fun unbox(x: Any): Any = TODO() - fun equals(): Boolean = true + fun equals(): Boolean = true } interface WithBox { diff --git a/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.kt b/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.kt index d8ff8e5ac72..bb069f97926 100644 --- a/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.kt +++ b/compiler/testData/diagnostics/tests/inlineClasses/reservedMembersAndConstructsInsideInlineClass.kt @@ -1,4 +1,4 @@ -// !LANGUAGE: +InlineClasses, -JvmInlineValueClasses +// !LANGUAGE: +InlineClasses, -JvmInlineValueClasses, +CustomEqualsInInlineClasses // !DIAGNOSTICS: -UNUSED_PARAMETER inline class IC1(val x: Any) { @@ -8,8 +8,8 @@ inline class IC1(val x: Any) { fun unbox() {} fun unbox(x: Any) {} - override fun equals(other: Any?): Boolean = true - override fun hashCode(): Int = 0 + override fun equals(other: Any?): Boolean = true + override fun hashCode(): Int = 0 } inline class IC2(val x: Any) { @@ -19,15 +19,15 @@ inline class IC2(val x: Any) { fun unbox(x: Any) {} fun unbox(): Any = TODO() - fun equals(my: Any, other: Any): Boolean = true - fun hashCode(a: Any): Int = 0 + fun equals(my: Any, other: Any): Boolean = true + fun hashCode(a: Any): Int = 0 } inline class IC3(val x: Any) { fun box(x: Any): Any = TODO() fun unbox(x: Any): Any = TODO() - fun equals(): Boolean = true + fun equals(): Boolean = true } interface WithBox { diff --git a/compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt b/compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt new file mode 100644 index 00000000000..d50022551b6 --- /dev/null +++ b/compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt @@ -0,0 +1,22 @@ +// FIR_IDENTICAL +// WITH_STDLIB +// !DIAGNOSTICS: -DEBUG_INFO_SMARTCAST +// LANGUAGE: +CustomEqualsInInlineClasses + + +@JvmInline +value class IC1(val x: Int) { + override fun equals(other: Any?) = true + + operator fun equals(other: IC1) = true + + override fun hashCode() = 0 +} + +@JvmInline +value class IC2(val x: Int) { + operator fun equals(other: IC1) = true + + operator fun equals(other: IC2) { + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.txt b/compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.txt new file mode 100644 index 00000000000..e304e652c55 --- /dev/null +++ b/compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.txt @@ -0,0 +1,20 @@ +package + +@kotlin.jvm.JvmInline public final value class IC1 { + public constructor IC1(/*0*/ x: kotlin.Int) + public final val x: kotlin.Int + public final operator fun equals(/*0*/ other: IC1): kotlin.Boolean + public open override /*1*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*synthesized*/ fun toString(): kotlin.String +} + +@kotlin.jvm.JvmInline public final value class IC2 { + public constructor IC2(/*0*/ x: kotlin.Int) + public final val x: kotlin.Int + public final operator fun equals(/*0*/ other: IC1): kotlin.Boolean + public final operator fun equals(/*0*/ other: IC2): kotlin.Unit + public open override /*1*/ /*synthesized*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*synthesized*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*synthesized*/ fun toString(): kotlin.String +} diff --git a/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.fir.kt b/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.fir.kt index 14d1cc834dd..05c21224ad9 100644 --- a/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.fir.kt +++ b/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.fir.kt @@ -1,5 +1,5 @@ // !SKIP_JAVAC -// !LANGUAGE: +InlineClasses +// !LANGUAGE: +InlineClasses, +CustomEqualsInInlineClasses // ALLOW_KOTLIN_PACKAGE // !DIAGNOSTICS: -UNUSED_PARAMETER @@ -15,8 +15,8 @@ value class IC1(val x: Any) { fun unbox() {} fun unbox(x: Any) {} - override fun equals(other: Any?): Boolean = true - override fun hashCode(): Int = 0 + override fun equals(other: Any?): Boolean = true + override fun hashCode(): Int = 0 } @JvmInline @@ -27,8 +27,8 @@ value class IC2(val x: Any) { fun unbox(x: Any) {} fun unbox(): Any = TODO() - fun equals(my: Any, other: Any): Boolean = true - fun hashCode(a: Any): Int = 0 + fun equals(my: Any, other: Any): Boolean = true + fun hashCode(a: Any): Int = 0 } @JvmInline @@ -36,7 +36,7 @@ value class IC3(val x: Any) { fun box(x: Any): Any = TODO() fun unbox(x: Any): Any = TODO() - fun equals(): Boolean = true + fun equals(): Boolean = true } interface WithBox { diff --git a/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.kt b/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.kt index bb70f5bc493..c020ac716d9 100644 --- a/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.kt +++ b/compiler/testData/diagnostics/tests/valueClasses/reservedMembersAndConstructsInsideValueClass.kt @@ -1,5 +1,5 @@ // !SKIP_JAVAC -// !LANGUAGE: +InlineClasses +// !LANGUAGE: +InlineClasses, +CustomEqualsInInlineClasses // ALLOW_KOTLIN_PACKAGE // !DIAGNOSTICS: -UNUSED_PARAMETER @@ -15,8 +15,8 @@ value class IC1(val x: Any) { fun unbox() {} fun unbox(x: Any) {} - override fun equals(other: Any?): Boolean = true - override fun hashCode(): Int = 0 + override fun equals(other: Any?): Boolean = true + override fun hashCode(): Int = 0 } @JvmInline @@ -27,8 +27,8 @@ value class IC2(val x: Any) { fun unbox(x: Any) {} fun unbox(): Any = TODO() - fun equals(my: Any, other: Any): Boolean = true - fun hashCode(a: Any): Int = 0 + fun equals(my: Any, other: Any): Boolean = true + fun hashCode(a: Any): Int = 0 } @JvmInline @@ -36,7 +36,7 @@ value class IC3(val x: Any) { fun box(x: Any): Any = TODO() fun unbox(x: Any): Any = TODO() - fun equals(): Boolean = true + fun equals(): Boolean = true } interface WithBox { diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java index 5ed95ca965e..4fc39ddd920 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java @@ -17860,6 +17860,12 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest { runTest("compiler/testData/diagnostics/tests/inlineClasses/identityComparisonWithInlineClasses.kt"); } + @Test + @TestMetadata("illegalEqualsOverridingInInlineClass.kt") + public void testIllegalEqualsOverridingInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/illegalEqualsOverridingInInlineClass.kt"); + } + @Test @TestMetadata("inlineClassCanImplementInterfaceByDelegation.kt") public void testInlineClassCanImplementInterfaceByDelegation() throws Exception { @@ -17968,6 +17974,12 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest { runTest("compiler/testData/diagnostics/tests/inlineClasses/synchronizedForbidden.kt"); } + @Test + @TestMetadata("typedEqualsOperatorModifierInInlineClass.kt") + public void testTypedEqualsOperatorModifierInInlineClass() throws Exception { + runTest("compiler/testData/diagnostics/tests/inlineClasses/typedEqualsOperatorModifierInInlineClass.kt"); + } + @Test @TestMetadata("unsignedLiteralsWithoutArtifactOnClasspath.kt") public void testUnsignedLiteralsWithoutArtifactOnClasspath() throws Exception { diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java index ae6d4335b49..0cfd34a70ad 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java @@ -20705,6 +20705,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassImplementsCollection.kt") public void testInlineClassImplementsCollection() throws Exception { diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java index 6260f06f7e7..fe6e4560f2a 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java @@ -21353,6 +21353,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes runTest("compiler/testData/codegen/box/inlineClasses/inlineClassAsLastExpressionInInLambdaGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassBothEqualsOverride.kt") + public void testInlineClassBothEqualsOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassBothEqualsOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassEqualityShouldUseTotalOrderForFloatingPointData.kt") public void testInlineClassEqualityShouldUseTotalOrderForFloatingPointData() throws Exception { @@ -21365,6 +21371,24 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualityShouldUseTotalOrderForFloatingPointDataGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassEqualsConsistency.kt") + public void testInlineClassEqualsConsistency() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualsConsistency.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + + @Test + @TestMetadata("inlineClassEqualsOverride.kt") + public void testInlineClassEqualsOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + + @Test + @TestMetadata("inlineClassEqualsOverridenForCollections.kt") + public void testInlineClassEqualsOverridenForCollections() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassEqualsOverridenForCollections.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassFieldHandling.kt") public void testInlineClassFieldHandling() throws Exception { @@ -21389,6 +21413,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassImplementsCollection.kt") public void testInlineClassImplementsCollection() throws Exception { @@ -21437,6 +21467,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes runTest("compiler/testData/codegen/box/inlineClasses/inlineClassPropertyReferenceGetAndSetGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @Test + @TestMetadata("inlineClassUntypedEqualsOverriden.kt") + public void testInlineClassUntypedEqualsOverriden() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassUntypedEqualsOverriden.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @Test @TestMetadata("inlineClassValueCapturedInInlineLambda.kt") public void testInlineClassValueCapturedInInlineLambda() throws Exception { diff --git a/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java b/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java index 42169916252..28578b4c8c3 100644 --- a/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java +++ b/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java @@ -17420,6 +17420,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); } + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); + } + @TestMetadata("inlineClassImplementsCollection.kt") public void testInlineClassImplementsCollection() throws Exception { runTest("compiler/testData/codegen/box/inlineClasses/inlineClassImplementsCollection.kt", TransformersFunctions.getReplaceOptionalJvmInlineAnnotationWithReal()); diff --git a/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt b/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt index 5ce4d461be2..0b51deb83af 100644 --- a/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt +++ b/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt @@ -284,6 +284,8 @@ enum class LanguageFeature( DataObjects(KOTLIN_1_9), // KT-4107 ProhibitAccessToEnumCompanionMembersInEnumConstructorCall(KOTLIN_1_9, kind = BUG_FIX), // KT-49110 ReferencesToSyntheticJavaProperties(KOTLIN_1_9), // KT-8575 + CustomEqualsInInlineClasses(KOTLIN_1_9, kind = UNSTABLE_FEATURE), // KT-24874 + // Disabled for indefinite time. See KT-53751 IgnoreNullabilityForErasedValueParameters(sinceVersion = null, kind = BUG_FIX), diff --git a/core/descriptors/src/org/jetbrains/kotlin/descriptors/descriptorUtil.kt b/core/descriptors/src/org/jetbrains/kotlin/descriptors/descriptorUtil.kt index 5d23b8e35f4..0e6cc16a657 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/descriptors/descriptorUtil.kt +++ b/core/descriptors/src/org/jetbrains/kotlin/descriptors/descriptorUtil.kt @@ -11,9 +11,13 @@ import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.resolve.isInlineClass import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.KotlinTypeFactory import org.jetbrains.kotlin.types.typeUtil.asTypeProjection +import org.jetbrains.kotlin.types.typeUtil.isBoolean +import org.jetbrains.kotlin.types.typeUtil.isNullableAny +import org.jetbrains.kotlin.util.OperatorNameConventions import org.jetbrains.kotlin.utils.sure import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -88,3 +92,14 @@ fun DeclarationDescriptor.containingPackage(): FqName? { } object DeserializedDeclarationsFromSupertypeConflictDataKey : CallableDescriptor.UserDataKey + +fun FunctionDescriptor.isTypedEqualsInInlineClass(): Boolean = name == OperatorNameConventions.EQUALS + && (returnType?.isBoolean() ?: false) && containingDeclaration.isInlineClass() + && valueParameters.size == 1 && valueParameters[0].type == (containingDeclaration as? ClassDescriptor)?.defaultType + && contextReceiverParameters.isEmpty() && extensionReceiverParameter == null + + +fun FunctionDescriptor.overridesEqualsFromAny(): Boolean = name == OperatorNameConventions.EQUALS + && (returnType?.isBoolean() ?: false) + && valueParameters.size == 1 && valueParameters[0].type.isNullableAny() + && contextReceiverParameters.isEmpty() && extensionReceiverParameter == null \ No newline at end of file diff --git a/core/descriptors/src/org/jetbrains/kotlin/util/modifierChecks.kt b/core/descriptors/src/org/jetbrains/kotlin/util/modifierChecks.kt index 5fb6beb6da0..0f61c7ed5e0 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/util/modifierChecks.kt +++ b/core/descriptors/src/org/jetbrains/kotlin/util/modifierChecks.kt @@ -24,6 +24,7 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns import org.jetbrains.kotlin.resolve.descriptorUtil.classId import org.jetbrains.kotlin.resolve.descriptorUtil.declaresOrInheritsDefaultValue import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.resolve.isInlineClass import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf @@ -198,8 +199,14 @@ object OperatorChecks : AbstractModifierChecks() { Checks(RANGE_UNTIL, MemberOrExtension, SingleValueParameter, NoDefaultAndVarargsCheck), Checks(EQUALS, Member) { fun DeclarationDescriptor.isAny() = this is ClassDescriptor && KotlinBuiltIns.isAny(this) - ensure(containingDeclaration.isAny() || overriddenDescriptors.any { it.containingDeclaration.isAny() }) { - "must override ''equals()'' in Any" + ensure(containingDeclaration.isAny() || overriddenDescriptors.any { it.containingDeclaration.isAny() } + || (containingDeclaration.isInlineClass() && isTypedEqualsInInlineClass())) { + buildString { + append("must override ''equals()'' in Any") + if (containingDeclaration.isInlineClass()) { + append(" or define ''equals(other: ${containingDeclaration.name}): Boolean''") + } + } } }, Checks(COMPARE_TO, MemberOrExtension, ReturnsInt, SingleValueParameter, NoDefaultAndVarargsCheck), diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/JsCodegenBoxTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/JsCodegenBoxTestGenerated.java index 85cbd17b49c..00d96fad6db 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/JsCodegenBoxTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/JsCodegenBoxTestGenerated.java @@ -16291,6 +16291,12 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest { runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); } + @Test + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); + } + @Test @TestMetadata("inlineClassInInitBlock.kt") public void testInlineClassInInitBlock() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsCodegenBoxTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsCodegenBoxTestGenerated.java index 9c3ae4395c8..68f4542ba05 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsCodegenBoxTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsCodegenBoxTestGenerated.java @@ -16309,6 +16309,12 @@ public class IrJsCodegenBoxTestGenerated extends AbstractIrJsCodegenBoxTest { runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); } + @Test + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); + } + @Test @TestMetadata("inlineClassInInitBlock.kt") public void testInlineClassInInitBlock() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenBoxWasmTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenBoxWasmTestGenerated.java index fe017ad7db5..9e088bea5b5 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenBoxWasmTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenBoxWasmTestGenerated.java @@ -14475,6 +14475,11 @@ public class IrCodegenBoxWasmTestGenerated extends AbstractIrCodegenBoxWasmTest runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); } + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); + } + @TestMetadata("inlineClassInInitBlock.kt") public void testInlineClassInInitBlock() throws Exception { runTest("compiler/testData/codegen/box/inlineClasses/inlineClassInInitBlock.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); diff --git a/native/native.tests/tests-gen/org/jetbrains/kotlin/konan/blackboxtest/NativeCodegenBoxTestGenerated.java b/native/native.tests/tests-gen/org/jetbrains/kotlin/konan/blackboxtest/NativeCodegenBoxTestGenerated.java index 012f44402d9..a2d139036e4 100644 --- a/native/native.tests/tests-gen/org/jetbrains/kotlin/konan/blackboxtest/NativeCodegenBoxTestGenerated.java +++ b/native/native.tests/tests-gen/org/jetbrains/kotlin/konan/blackboxtest/NativeCodegenBoxTestGenerated.java @@ -16706,6 +16706,7 @@ public class NativeCodegenBoxTestGenerated extends AbstractNativeCodegenBoxTest register("compiler/testData/codegen/box/inlineClasses/inlineClassFieldHandlingGeneric.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); register("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvoke.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); register("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); + register("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); register("compiler/testData/codegen/box/inlineClasses/inlineClassInInitBlock.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); register("compiler/testData/codegen/box/inlineClasses/inlineClassInInitBlockGeneric.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); register("compiler/testData/codegen/box/inlineClasses/inlineClassInStringTemplate.kt", TransformersFunctions.getRemoveOptionalJvmInlineAnnotation()); @@ -17935,6 +17936,13 @@ public class NativeCodegenBoxTestGenerated extends AbstractNativeCodegenBoxTest runTest("compiler/testData/codegen/box/inlineClasses/inlineClassFunctionInvokeGeneric.kt"); } + @Test + @TestMetadata("inlineClassHashCodeOverride.kt") + public void testInlineClassHashCodeOverride() throws Exception { + // There is a registered source transformer for the testcase: TransformersFunctions.getRemoveOptionalJvmInlineAnnotation() + runTest("compiler/testData/codegen/box/inlineClasses/inlineClassHashCodeOverride.kt"); + } + @Test @TestMetadata("inlineClassInInitBlock.kt") public void testInlineClassInInitBlock() throws Exception {