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 {