WIP! Fri  4 Aug 20:17:06 CEST 2023
diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java
index 9087a52..75c4433 100644
--- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java
+++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java
@@ -1085,6 +1085,12 @@
         }
 
         @Test
+        @TestMetadata("injectGenericUpperBoundOverload.kt")
+        public void testInjectGenericUpperBoundOverload() throws Exception {
+            runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectGenericUpperBoundOverload.kt");
+        }
+
+        @Test
         @TestMetadata("injectInnerClass.kt")
         public void testInjectInnerClass() throws Exception {
             runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectInnerClass.kt");
@@ -1157,6 +1163,12 @@
         }
 
         @Test
+        @TestMetadata("propertyKindOverride.kt")
+        public void testPropertyKindOverride() throws Exception {
+            runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.kt");
+        }
+
+        @Test
         @TestMetadata("severalMismatches.kt")
         public void testSeveralMismatches() throws Exception {
             runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/severalMismatches.kt");
diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java
index e7bd5b9..78e5035 100644
--- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java
+++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java
@@ -1085,6 +1085,12 @@
         }
 
         @Test
+        @TestMetadata("injectGenericUpperBoundOverload.kt")
+        public void testInjectGenericUpperBoundOverload() throws Exception {
+            runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectGenericUpperBoundOverload.kt");
+        }
+
+        @Test
         @TestMetadata("injectInnerClass.kt")
         public void testInjectInnerClass() throws Exception {
             runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectInnerClass.kt");
@@ -1157,6 +1163,12 @@
         }
 
         @Test
+        @TestMetadata("propertyKindOverride.kt")
+        public void testPropertyKindOverride() throws Exception {
+            runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.kt");
+        }
+
+        @Test
         @TestMetadata("severalMismatches.kt")
         public void testSeveralMismatches() throws Exception {
             runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/severalMismatches.kt");
diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt
index 8ee3a38..142a5bf 100644
--- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt
+++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt
@@ -150,6 +150,7 @@
         expect.unsubstitutedScope(context.session, context.scopeSession, withForcedTypeCalculator = false, memberRequiredPhase = null)
     val actualScope =
         actual.unsubstitutedScope(context.session, context.scopeSession, withForcedTypeCalculator = false, memberRequiredPhase = null)
+
     val classTypeSubstitutor =
         createExpectActualTypeParameterSubstitutor(expect.typeParameterSymbols, actual.typeParameterSymbols, context.session)
     val expectClassCallables = expectScope.extractNonPrivateCallables(classTypeSubstitutor, ExpectActual.EXPECT)
@@ -292,4 +293,5 @@
         ExpectActualMemberDiff.Kind.ModalityChangedInOverride -> FirErrors.MODALITY_CHANGED_IN_NON_FINAL_EXPECT_CLASSIFIER_ACTUALIZATION
         ExpectActualMemberDiff.Kind.VisibilityChangedInOverride -> FirErrors.VISIBILITY_CHANGED_IN_NON_FINAL_EXPECT_CLASSIFIER_ACTUALIZATION
         ExpectActualMemberDiff.Kind.ParameterNameChangedInOverride -> FirErrors.PARAMETER_NAME_CHANGED_IN_NON_FINAL_EXPECT_CLASSIFIER_ACTUALIZATION
+        ExpectActualMemberDiff.Kind.PropertyKindChangedInOverride -> TODO()
     }
diff --git a/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/ExpectActualUtils.kt b/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/ExpectActualUtils.kt
index 281f344..f291fe7 100644
--- a/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/ExpectActualUtils.kt
+++ b/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/ExpectActualUtils.kt
@@ -31,6 +31,7 @@
     return substitutor.chain(parentSubstitutor)
 }
 
+// note: copy-pasted to: org.jetbrains.kotlin.fir.resolve.transformers.mpp.FirExpectActualMatchingContextImpl.areCompatibleExpectActualTypes
 fun areCompatibleExpectActualTypes(
     expectedType: ConeKotlinType?,
     actualType: ConeKotlinType?,
diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/mpp/FirExpectActualMatchingContextImpl.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/mpp/FirExpectActualMatchingContextImpl.kt
index 818d007..8a08938 100644
--- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/mpp/FirExpectActualMatchingContextImpl.kt
+++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/mpp/FirExpectActualMatchingContextImpl.kt
@@ -286,6 +286,7 @@
     override val TypeParameterSymbolMarker.isReified: Boolean
         get() = asSymbol().isReified
 
+    // note: copy-pasted to: org.jetbrains.kotlin.fir.types.ExpectActualUtilsKt.areCompatibleExpectActualTypes
     override fun areCompatibleExpectActualTypes(
         expectType: KotlinTypeMarker?,
         actualType: KotlinTypeMarker?,
diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt
index 7217458..2e88090 100644
--- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt
+++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ActualClassifierMustHasTheSameMembersAsNonFinalExpectClassifierChecker.kt
@@ -19,9 +19,11 @@
 import org.jetbrains.kotlin.resolve.multiplatform.ExpectActualCompatibility
 import org.jetbrains.kotlin.resolve.multiplatform.ExpectActualMemberDiff
 import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver
+import org.jetbrains.kotlin.resolve.multiplatform.createExpectActualTypeParameterSubstitutor
 import org.jetbrains.kotlin.resolve.scopes.MemberScope
 import org.jetbrains.kotlin.resolve.source.KotlinSourceElement
 import org.jetbrains.kotlin.types.KotlinType
+import org.jetbrains.kotlin.types.TypeSubstitutor
 import java.util.*
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
@@ -132,82 +134,118 @@
 ): Set<ExpectActualMemberDiff<CallableMemberDescriptor, ClassDescriptor>> {
     val expectScope = expect.unsubstitutedMemberScope
     val actualScope = actual.unsubstitutedMemberScope
-    val expectClassCallables = expectScope.extractNonPrivateCallables()
+
+    val classTypeSubstitutor = createExpectActualTypeParameterSubstitutor(
+        expect.declaredTypeParameters,
+        actual.declaredTypeParameters,
+        parentSubstitutor = null
+    )
+    val expectClassCallables = expectScope.extractNonPrivateCallables(classTypeSubstitutor, ExpectActual.EXPECT)
     val nameAndKindToExpectCallable = expectClassCallables.groupBy { it.name to it.kind }
-    return (actualScope.extractNonPrivateCallables() - expectClassCallables).asSequence()
+    return (actualScope.extractNonPrivateCallables(classTypeSubstitutor, ExpectActual.ACTUAL) - expectClassCallables).asSequence()
         .flatMap { unmatchedActualCallable ->
             when (val expectCallablesWithTheSameName =
                 nameAndKindToExpectCallable[unmatchedActualCallable.name to unmatchedActualCallable.kind]) {
                 null -> listOf(ExpectActualMemberDiff.Kind.NonPrivateCallableAdded)
                 else -> expectCallablesWithTheSameName.map {
-                    calculateDiffKind(expect = it, actual = unmatchedActualCallable) ?: error("Not equal callables can't have zero diff")
+                    calculateExpectActualMemberDiffKind(expect = it, actual = unmatchedActualCallable)
+                        ?: error("Not equal callables can't have zero diff")
                 }
             }.map { kind -> ExpectActualMemberDiff(kind, unmatchedActualCallable.descriptor, expect) }
         }
         .toSet()
 }
 
-private fun MemberScope.extractNonPrivateCallables(): Set<Callable> {
+private fun MemberScope.extractNonPrivateCallables(
+    classTypeSubstitutor: TypeSubstitutor,
+    expectActual: ExpectActual
+): Set<Callable> {
     val functions =
         getFunctionNames().asSequence().flatMap { getContributedFunctions(it, NoLookupLocation.FROM_FRONTEND_CHECKER) }
     val properties =
         getVariableNames().asSequence().flatMap { getContributedVariables(it, NoLookupLocation.FROM_FRONTEND_CHECKER) }
     return (functions + properties).filter { !Visibilities.isPrivate(it.visibility.delegate) }
-        .map { descriptor ->
-            val returnType = descriptor.returnType
-            Callable(
-                descriptor.name,
-                when (descriptor) {
-                    is PropertyDescriptor -> Kind.PROPERTY
-                    is FunctionDescriptor -> Kind.FUNCTION
-                    else -> error("Unknown kind $descriptor")
-                },
-                descriptor.extensionReceiverParameter?.type,
-                descriptor.contextReceiverParameters.map(ValueDescriptor::getType),
-                descriptor.valueParameters.map { Parameter(it.name, it.type) },
-                returnType ?: error("Can't get return type"),
-                descriptor.modality,
-                descriptor.visibility.delegate,
-                descriptor,
-            )
-        }
+        .map { descriptor -> Callable(descriptor, expectActual, classTypeSubstitutor) }
         .toSet()
 }
 
 private data class Parameter(val name: Name, val type: KotlinType)
 private enum class Kind { FUNCTION, PROPERTY }
+private enum class ExpectActual { EXPECT, ACTUAL }
 private class Callable(
-    val name: Name,
-    val kind: Kind,
-    val extensionReceiverType: KotlinType?,
-    val contextReceiverTypes: List<KotlinType>,
-    val parameters: List<Parameter>,
-    val returnType: KotlinType,
-    val modality: Modality,
-    val visibility: Visibility,
     val descriptor: CallableMemberDescriptor,
+    val expectActual: ExpectActual,
+    val classTypeSubstitutor: TypeSubstitutor,
 ) {
-    override fun equals(other: Any?): Boolean = other is Callable && calculateDiffKind(this, other) == null
-    override fun hashCode(): Int =
-        Objects.hash(name, kind, extensionReceiverType, contextReceiverTypes, parameters, returnType, modality, visibility)
+    val name: Name = descriptor.name
+    val kind: Kind = when (descriptor) {
+        is PropertyDescriptor -> Kind.PROPERTY
+        is FunctionDescriptor -> Kind.FUNCTION
+        else -> error("Unknown kind $descriptor")
+    }
+    val extensionReceiverType: KotlinType? = descriptor.extensionReceiverParameter?.type
+    val contextReceiverTypes: List<KotlinType> = descriptor.contextReceiverParameters.map(ValueDescriptor::getType)
+    val parameters: List<Parameter> = descriptor.valueParameters.map { Parameter(it.name, it.type) }
+    val returnType: KotlinType = descriptor.returnType ?: error("Can't get return type")
+    val modality: Modality = descriptor.modality
+    val visibility: Visibility = descriptor.visibility.delegate
+
+    override fun equals(other: Any?): Boolean {
+        if (other !is Callable) return false
+        check(classTypeSubstitutor === other.classTypeSubstitutor)
+        return if (expectActual == other.expectActual) {
+            name == other.name &&
+                    kind == other.kind &&
+                    extensionReceiverType == other.extensionReceiverType &&
+                    contextReceiverTypes == other.contextReceiverTypes &&
+                    parameters == other.parameters &&
+                    returnType == other.returnType &&
+                    modality == other.modality &&
+                    visibility == other.visibility
+        } else {
+            val (expect, actual) = if (expectActual == ExpectActual.EXPECT) this to other else other to this
+            calculateExpectActualMemberDiffKind(expect, actual) == null
+        }
+    }
+
+    override fun hashCode(): Int = // Don't hash types because type comparison is complicated
+        Objects.hash(
+            name,
+            kind,
+            extensionReceiverType != null,
+            contextReceiverTypes.size,
+            parameters.map(Parameter::name),
+            modality,
+            visibility
+        )
 }
 
 private val CallableMemberDescriptor.psiIfReal: KtCallableDeclaration?
     get() = takeIf { it.kind.isReal }?.source?.let { it as? KotlinSourceElement }?.psi as? KtCallableDeclaration
 
-private fun calculateDiffKind(expect: Callable, actual: Callable): ExpectActualMemberDiff.Kind? = when {
-    expect.name != actual.name ||
-            expect.kind != actual.kind ||
-            expect.extensionReceiverType != actual.extensionReceiverType ||
-            expect.contextReceiverTypes != actual.contextReceiverTypes ||
-            expect.parameters.map(Parameter::type) != actual.parameters.map(Parameter::type) ->
-        ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
-    expect.parameters.map(Parameter::name) != actual.parameters.map(Parameter::name) ->
-        ExpectActualMemberDiff.Kind.ParameterNameChangedInOverride
-    expect.returnType != actual.returnType -> ExpectActualMemberDiff.Kind.ReturnTypeCovariantOverride
-    expect.modality != actual.modality -> ExpectActualMemberDiff.Kind.ModalityChangedInOverride
-    expect.visibility != actual.visibility -> ExpectActualMemberDiff.Kind.VisibilityChangedInOverride
-    else -> null
+private fun calculateExpectActualMemberDiffKind(expect: Callable, actual: Callable): ExpectActualMemberDiff.Kind? {
+    check(expect.expectActual == ExpectActual.EXPECT)
+    check(actual.expectActual == ExpectActual.ACTUAL)
+    check(expect.classTypeSubstitutor === actual.classTypeSubstitutor)
+    val substitutor = createExpectActualTypeParameterSubstitutor(
+        expect.descriptor.typeParameters,
+        actual.descriptor.typeParameters,
+        actual.classTypeSubstitutor
+    )
+    return when {
+        expect.name != actual.name ||
+                expect.kind != actual.kind ||
+                expect.extensionReceiverType != actual.extensionReceiverType ||
+                expect.contextReceiverTypes != actual.contextReceiverTypes ||
+                expect.parameters.map(Parameter::type) != actual.parameters.map(Parameter::type) ->
+            ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
+        expect.parameters.map(Parameter::name) != actual.parameters.map(Parameter::name) ->
+            ExpectActualMemberDiff.Kind.ParameterNameChangedInOverride
+        expect.returnType != actual.returnType -> ExpectActualMemberDiff.Kind.ReturnTypeCovariantOverride
+        expect.modality != actual.modality -> ExpectActualMemberDiff.Kind.ModalityChangedInOverride
+        expect.visibility != actual.visibility -> ExpectActualMemberDiff.Kind.VisibilityChangedInOverride
+        else -> null
+    }
 }
 
 private val ExpectActualMemberDiff.Kind.factory: DiagnosticFactory1<KtCallableDeclaration, ExpectActualMemberDiff<CallableMemberDescriptor, ClassDescriptor>>
@@ -217,4 +255,5 @@
         ExpectActualMemberDiff.Kind.ModalityChangedInOverride -> Errors.MODALITY_CHANGED_IN_NON_FINAL_EXPECT_CLASSIFIER_ACTUALIZATION
         ExpectActualMemberDiff.Kind.VisibilityChangedInOverride -> Errors.VISIBILITY_CHANGED_IN_NON_FINAL_EXPECT_CLASSIFIER_ACTUALIZATION
         ExpectActualMemberDiff.Kind.ParameterNameChangedInOverride -> Errors.PARAMETER_NAME_CHANGED_IN_NON_FINAL_EXPECT_CLASSIFIER_ACTUALIZATION
+        ExpectActualMemberDiff.Kind.PropertyKindChangedInOverride -> TODO()
     }
diff --git a/compiler/resolution/src/org/jetbrains/kotlin/resolve/multiplatform/ClassicExpectActualMatchingContext.kt b/compiler/resolution/src/org/jetbrains/kotlin/resolve/multiplatform/ClassicExpectActualMatchingContext.kt
index 1412dc8..8961b1c 100644
--- a/compiler/resolution/src/org/jetbrains/kotlin/resolve/multiplatform/ClassicExpectActualMatchingContext.kt
+++ b/compiler/resolution/src/org/jetbrains/kotlin/resolve/multiplatform/ClassicExpectActualMatchingContext.kt
@@ -19,7 +19,6 @@
 import org.jetbrains.kotlin.resolve.calls.mpp.ExpectActualMatchingContext.AnnotationCallInfo
 import org.jetbrains.kotlin.resolve.checkers.OptInNames
 import org.jetbrains.kotlin.resolve.descriptorUtil.*
-import org.jetbrains.kotlin.resolve.findTopMostOverriddenDescriptors
 import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
 import org.jetbrains.kotlin.resolve.scopes.MemberScope
 import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered
@@ -144,20 +143,11 @@
         expectTypeParameters: List<TypeParameterSymbolMarker>,
         actualTypeParameters: List<TypeParameterSymbolMarker>,
         parentSubstitutor: TypeSubstitutorMarker?,
-    ): TypeSubstitutorMarker {
-        val expectParameters = expectTypeParameters.castAll<TypeParameterDescriptor>()
-        val actualParameters = actualTypeParameters.castAll<TypeParameterDescriptor>()
-        val substitutor = TypeSubstitutor.create(
-            TypeConstructorSubstitution.createByParametersMap(expectParameters.keysToMap {
-                actualParameters[it.index].defaultType.asTypeProjection()
-            })
-        )
-        return when (parentSubstitutor) {
-            null -> substitutor
-            is TypeSubstitutor -> TypeSubstitutor.createChainedSubstitutor(parentSubstitutor.substitution, substitutor.substitution)
-            else -> error("Unsupported substitutor type: $parentSubstitutor")
-        }
-    }
+    ): TypeSubstitutorMarker = org.jetbrains.kotlin.resolve.multiplatform.createExpectActualTypeParameterSubstitutor(
+        expectTypeParameters.castAll<TypeParameterDescriptor>(),
+        actualTypeParameters.castAll<TypeParameterDescriptor>(),
+        parentSubstitutor as? TypeSubstitutor ?: error("Unsupported substitutor type: $parentSubstitutor")
+    )
 
     override fun RegularClassSymbolMarker.collectAllMembers(isActualDeclaration: Boolean): List<DeclarationSymbolMarker> {
         return asDescriptor().getMembers(name = null)
@@ -221,15 +211,8 @@
         get() = asDescriptor().isReified
 
     override fun areCompatibleExpectActualTypes(expectType: KotlinTypeMarker?, actualType: KotlinTypeMarker?): Boolean {
-        if (expectType == null) return actualType == null
-        if (actualType == null) return false
-
         require(expectType is KotlinType && actualType is KotlinType)
-        return if (platformModule.isTypeRefinementEnabled()) {
-            areCompatibleTypesViaTypeRefinement(expectType, actualType)
-        } else {
-            areCompatibleTypesViaTypeContext(expectType, actualType)
-        }
+        return areCompatibleExpectActualTypes(expectType, actualType)
     }
 
     override val RegularClassSymbolMarker.defaultType: KotlinTypeMarker
@@ -387,3 +370,30 @@
                     this is CallableMemberDescriptor && kind == CallableMemberDescriptor.Kind.SYNTHESIZED
         }
 }
+
+fun createExpectActualTypeParameterSubstitutor(
+    expectParameters: List<TypeParameterDescriptor>,
+    actualParameters: List<TypeParameterDescriptor>,
+    parentSubstitutor: TypeSubstitutor?,
+): TypeSubstitutor {
+    val substitutor = TypeSubstitutor.create(
+        TypeConstructorSubstitution.createByParametersMap(expectParameters.keysToMap {
+            actualParameters[it.index].defaultType.asTypeProjection()
+        })
+    )
+    return when (parentSubstitutor) {
+        null -> substitutor
+        else -> TypeSubstitutor.createChainedSubstitutor(parentSubstitutor.substitution, substitutor.substitution)
+    }
+}
+
+fun areCompatibleExpectActualTypes(expectType: KotlinType?, actualType: KotlinType?, platformModule: ModuleDescriptor): Boolean {
+    if (expectType == null) return actualType == null
+    if (actualType == null) return false
+
+    return if (platformModule.isTypeRefinementEnabled()) {
+        areCompatibleTypesViaTypeRefinement(expectType, actualType)
+    } else {
+        areCompatibleTypesViaTypeContext(expectType, actualType)
+    }
+}
diff --git a/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectGenericUpperBoundOverload.kt b/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectGenericUpperBoundOverload.kt
new file mode 100644
index 0000000..e2a41e1
--- /dev/null
+++ b/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectGenericUpperBoundOverload.kt
@@ -0,0 +1,16 @@
+// FIR_IDENTICAL
+// MODULE: m1-common
+// FILE: common.kt
+
+open class Base {
+    open fun <T> foo(t: T) {}
+}
+
+expect class Foo : Base
+
+// MODULE: m2-jvm()()(m1-common)
+// FILE: jvm.kt
+
+actual class Foo : Base() {
+    fun <T : Comparable<T>> <!NON_ACTUAL_MEMBER_DECLARED_IN_EXPECT_NON_FINAL_CLASSIFIER_ACTUALIZATION!>foo<!>(t: T) {}
+}
diff --git a/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.fir.kt b/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.fir.kt
new file mode 100644
index 0000000..a2d3afc
--- /dev/null
+++ b/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.fir.kt
@@ -0,0 +1,15 @@
+// MODULE: m1-common
+// FILE: common.kt
+
+open class Base {
+    open val foo: Int = 1
+}
+
+expect open class Foo : Base
+
+// MODULE: m2-jvm()()(m1-common)
+// FILE: jvm.kt
+
+actual open class Foo : Base() {
+    override var foo: Int = 1
+}
\ No newline at end of file
diff --git a/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.kt b/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.kt
new file mode 100644
index 0000000..aa1214a
--- /dev/null
+++ b/compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.kt
@@ -0,0 +1,15 @@
+// MODULE: m1-common
+// FILE: common.kt
+
+open class Base {
+    open val foo: Int = 1
+}
+
+expect open class Foo : Base
+
+// MODULE: m2-jvm()()(m1-common)
+// FILE: jvm.kt
+
+actual open class Foo : Base() {
+    override var <!NON_ACTUAL_MEMBER_DECLARED_IN_EXPECT_NON_FINAL_CLASSIFIER_ACTUALIZATION!>foo<!>: Int = 1
+}
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 336c73f..3286c68 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
@@ -23740,6 +23740,12 @@
                 }
 
                 @Test
+                @TestMetadata("injectGenericUpperBoundOverload.kt")
+                public void testInjectGenericUpperBoundOverload() throws Exception {
+                    runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectGenericUpperBoundOverload.kt");
+                }
+
+                @Test
                 @TestMetadata("injectInnerClass.kt")
                 public void testInjectInnerClass() throws Exception {
                     runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/injectInnerClass.kt");
@@ -23812,6 +23818,12 @@
                 }
 
                 @Test
+                @TestMetadata("propertyKindOverride.kt")
+                public void testPropertyKindOverride() throws Exception {
+                    runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/propertyKindOverride.kt");
+                }
+
+                @Test
                 @TestMetadata("severalMismatches.kt")
                 public void testSeveralMismatches() throws Exception {
                     runTest("compiler/testData/diagnostics/tests/multiplatform/injectIntoExpectOpenClassDuringActualization/severalMismatches.kt");
diff --git a/core/compiler.common/src/org/jetbrains/kotlin/resolve/multiplatform/ExpectActualMemberDiff.kt b/core/compiler.common/src/org/jetbrains/kotlin/resolve/multiplatform/ExpectActualMemberDiff.kt
index 30bc451..a0d9374 100644
--- a/core/compiler.common/src/org/jetbrains/kotlin/resolve/multiplatform/ExpectActualMemberDiff.kt
+++ b/core/compiler.common/src/org/jetbrains/kotlin/resolve/multiplatform/ExpectActualMemberDiff.kt
@@ -8,6 +8,8 @@
 data class ExpectActualMemberDiff<out M, out C>(val kind: Kind, val actualMember: M, val expectClass: C) {
     /**
      * Diff kinds that are legal for fake-overrides in final `expect class`, but illegal for non-final `expect class`
+     *
+     * Also see: [toDiffKind]
      */
     enum class Kind(val rawMessage: String) {
         NonPrivateCallableAdded(
@@ -35,6 +37,50 @@
                     "This error happens because the expect class ''{1}'' is non-final."
         ),
 
-        // todo type parameters?
+        PropertyKindChangedInOverride(
+            "{0}: The property kind (val vs var) of this member must be the same in the expect class and the actual class. " +
+                    "This error happens because the expect class ''{1}'' is non-final."
+        ),
+    }
+}
+
+/**
+ * This function serves a purpose of type-safe documentation. A mapping can be established between [ExpectActualCompatibility.Incompatible]
+ * and [ExpectActualMemberDiff.Kind]. This exhaustive when fixates this mapping, ensuring that we won't forget to add new
+ * [ExpectActualMemberDiff.Kind] when [ExpectActualCompatibility.Incompatible] is updated and vice versa.
+ */
+@Suppress("unused")
+private fun ExpectActualCompatibility.Incompatible<*>.toDiffKind(): ExpectActualMemberDiff.Kind? {
+    return when (this) {
+        ExpectActualCompatibility.Incompatible.CallableKind -> ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
+        ExpectActualCompatibility.Incompatible.ParameterCount -> ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
+        ExpectActualCompatibility.Incompatible.ParameterShape -> ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
+        ExpectActualCompatibility.Incompatible.ParameterTypes -> ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
+        ExpectActualCompatibility.Incompatible.ReturnType -> ExpectActualMemberDiff.Kind.ReturnTypeCovariantOverride
+        ExpectActualCompatibility.Incompatible.TypeParameterCount -> ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
+        ExpectActualCompatibility.Incompatible.TypeParameterUpperBounds -> ExpectActualMemberDiff.Kind.NonPrivateCallableAdded
+        ExpectActualCompatibility.Incompatible.ActualFunctionWithDefaultParameters -> null // It's not possible to add default parameters in override
+        ExpectActualCompatibility.Incompatible.ClassKind -> null // Not applicable because ExpectActualMemberDiff is about members
+        ExpectActualCompatibility.Incompatible.ClassModifiers -> null // Not applicable because ExpectActualMemberDiff is about members
+        is ExpectActualCompatibility.Incompatible.ClassScopes -> null // Not applicable because ExpectActualMemberDiff is about members
+        ExpectActualCompatibility.Incompatible.EnumEntries -> null // Not applicable because ExpectActualMemberDiff is about members
+        ExpectActualCompatibility.Incompatible.FunInterfaceModifier -> null // Not applicable because ExpectActualMemberDiff is about members
+        ExpectActualCompatibility.Incompatible.FunctionModifiersDifferent -> null // It's not possible to override with different function modifier
+        ExpectActualCompatibility.Incompatible.FunctionModifiersNotSubset -> null // It's not possible to override with different function modifier
+        ExpectActualCompatibility.Incompatible.Modality -> ExpectActualMemberDiff.Kind.ModalityChangedInOverride
+        ExpectActualCompatibility.Incompatible.ParameterNames -> ExpectActualMemberDiff.Kind.ParameterNameChangedInOverride
+        ExpectActualCompatibility.Incompatible.PropertyConstModifier -> null // const can't be overridden
+        ExpectActualCompatibility.Incompatible.PropertyKind -> ExpectActualMemberDiff.Kind.PropertyKindChangedInOverride
+        ExpectActualCompatibility.Incompatible.PropertyLateinitModifier -> TODO()
+        ExpectActualCompatibility.Incompatible.PropertySetterVisibility -> TODO()
+        ExpectActualCompatibility.Incompatible.Supertypes -> null // Not applicable because ExpectActualMemberDiff is about members. But related: ACTUAL_CLASSIFIER_MUST_HAVE_THE_SAME_SUPERTYPES_AS_NON_FINAL_EXPECT_CLASSIFIER
+        ExpectActualCompatibility.Incompatible.TypeParameterNames -> TODO()
+        ExpectActualCompatibility.Incompatible.TypeParameterReified -> TODO()
+        ExpectActualCompatibility.Incompatible.TypeParameterVariance -> TODO()
+        ExpectActualCompatibility.Incompatible.Unknown -> TODO()
+        ExpectActualCompatibility.Incompatible.ValueParameterCrossinline -> TODO()
+        ExpectActualCompatibility.Incompatible.ValueParameterNoinline -> TODO()
+        ExpectActualCompatibility.Incompatible.ValueParameterVararg -> TODO()
+        ExpectActualCompatibility.Incompatible.Visibility -> TODO()
     }
 }