[FE] Implement FE logic of expect actual annotations matching

This implementation only checks annotations set on expect/actual
declarations and requires further refinement (e.g. checking of other
annotation targets, class scopes within typealiases).

^KT-58551
This commit is contained in:
Roman Efremov
2023-06-22 15:22:17 +02:00
committed by Space Team
parent b1bdb619d8
commit 1a4ab9bb4b
49 changed files with 1481 additions and 25 deletions
@@ -205,6 +205,100 @@ public class FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated extends Abst
runTest("compiler/testData/diagnostics/tests/multiplatform/widerVisibilityInActualClassifier.kt");
}
@Nested
@TestMetadata("compiler/testData/diagnostics/tests/multiplatform/annotationMatching")
@TestDataPath("$PROJECT_ROOT")
public class AnnotationMatching {
@Test
public void testAllFilesPresentInAnnotationMatching() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/diagnostics/tests/multiplatform/annotationMatching"), Pattern.compile("^(.*)\\.kts?$"), Pattern.compile("^(.+)\\.(reversed|fir|ll)\\.kts?$"), TargetBackend.JVM_IR, true);
}
@Test
@TestMetadata("annotationArgumentsConstExpressions.kt")
public void testAnnotationArgumentsConstExpressions() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/annotationArgumentsConstExpressions.kt");
}
@Test
@TestMetadata("annotationArgumentsDefaults.kt")
public void testAnnotationArgumentsDefaults() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/annotationArgumentsDefaults.kt");
}
@Test
@TestMetadata("annotationTypeParameters.kt")
public void testAnnotationTypeParameters() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/annotationTypeParameters.kt");
}
@Test
@TestMetadata("basicOnDeclaration.kt")
public void testBasicOnDeclaration() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/basicOnDeclaration.kt");
}
@Test
@TestMetadata("checkDiagnosticFullText.kt")
public void testCheckDiagnosticFullText() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/checkDiagnosticFullText.kt");
}
@Test
@TestMetadata("compatibleOverrides.kt")
public void testCompatibleOverrides() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/compatibleOverrides.kt");
}
@Test
@TestMetadata("differentOrder.kt")
public void testDifferentOrder() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/differentOrder.kt");
}
@Test
@TestMetadata("floatNumbersComparison.kt")
public void testFloatNumbersComparison() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/floatNumbersComparison.kt");
}
@Test
@TestMetadata("kclassArgWithExpectClass.kt")
public void testKclassArgWithExpectClass() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/kclassArgWithExpectClass.kt");
}
@Test
@TestMetadata("kotlinAnaloguesForJavaAnnotations.kt")
public void testKotlinAnaloguesForJavaAnnotations() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/kotlinAnaloguesForJavaAnnotations.kt");
}
@Test
@TestMetadata("typealias.kt")
public void testTypealias() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/typealias.kt");
}
@Test
@TestMetadata("typealiasToJavaLibrary.kt")
public void testTypealiasToJavaLibrary() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/typealiasToJavaLibrary.kt");
}
@Test
@TestMetadata("typealiasToKtLibrary.kt")
public void testTypealiasToKtLibrary() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/typealiasToKtLibrary.kt");
}
@Test
@TestMetadata("withOtherIncomatibilities.kt")
public void testWithOtherIncomatibilities() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/withOtherIncomatibilities.kt");
}
}
@Nested
@TestMetadata("compiler/testData/diagnostics/tests/multiplatform/defaultArguments")
@TestDataPath("$PROJECT_ROOT")
@@ -205,6 +205,100 @@ public class FirOldFrontendMPPDiagnosticsWithPsiTestGenerated extends AbstractFi
runTest("compiler/testData/diagnostics/tests/multiplatform/widerVisibilityInActualClassifier.kt");
}
@Nested
@TestMetadata("compiler/testData/diagnostics/tests/multiplatform/annotationMatching")
@TestDataPath("$PROJECT_ROOT")
public class AnnotationMatching {
@Test
public void testAllFilesPresentInAnnotationMatching() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/diagnostics/tests/multiplatform/annotationMatching"), Pattern.compile("^(.*)\\.kts?$"), Pattern.compile("^(.+)\\.(reversed|fir|ll)\\.kts?$"), TargetBackend.JVM_IR, true);
}
@Test
@TestMetadata("annotationArgumentsConstExpressions.kt")
public void testAnnotationArgumentsConstExpressions() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/annotationArgumentsConstExpressions.kt");
}
@Test
@TestMetadata("annotationArgumentsDefaults.kt")
public void testAnnotationArgumentsDefaults() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/annotationArgumentsDefaults.kt");
}
@Test
@TestMetadata("annotationTypeParameters.kt")
public void testAnnotationTypeParameters() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/annotationTypeParameters.kt");
}
@Test
@TestMetadata("basicOnDeclaration.kt")
public void testBasicOnDeclaration() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/basicOnDeclaration.kt");
}
@Test
@TestMetadata("checkDiagnosticFullText.kt")
public void testCheckDiagnosticFullText() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/checkDiagnosticFullText.kt");
}
@Test
@TestMetadata("compatibleOverrides.kt")
public void testCompatibleOverrides() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/compatibleOverrides.kt");
}
@Test
@TestMetadata("differentOrder.kt")
public void testDifferentOrder() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/differentOrder.kt");
}
@Test
@TestMetadata("floatNumbersComparison.kt")
public void testFloatNumbersComparison() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/floatNumbersComparison.kt");
}
@Test
@TestMetadata("kclassArgWithExpectClass.kt")
public void testKclassArgWithExpectClass() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/kclassArgWithExpectClass.kt");
}
@Test
@TestMetadata("kotlinAnaloguesForJavaAnnotations.kt")
public void testKotlinAnaloguesForJavaAnnotations() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/kotlinAnaloguesForJavaAnnotations.kt");
}
@Test
@TestMetadata("typealias.kt")
public void testTypealias() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/typealias.kt");
}
@Test
@TestMetadata("typealiasToJavaLibrary.kt")
public void testTypealiasToJavaLibrary() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/typealiasToJavaLibrary.kt");
}
@Test
@TestMetadata("typealiasToKtLibrary.kt")
public void testTypealiasToKtLibrary() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/typealiasToKtLibrary.kt");
}
@Test
@TestMetadata("withOtherIncomatibilities.kt")
public void testWithOtherIncomatibilities() throws Exception {
runTest("compiler/testData/diagnostics/tests/multiplatform/annotationMatching/withOtherIncomatibilities.kt");
}
}
@Nested
@TestMetadata("compiler/testData/diagnostics/tests/multiplatform/defaultArguments")
@TestDataPath("$PROJECT_ROOT")
@@ -1180,6 +1180,11 @@ object DIAGNOSTICS_LIST : DiagnosticList("FirErrors") {
val ACTUAL_TYPEALIAS_TO_SPECIAL_ANNOTATION by error<KtTypeAlias>(PositioningStrategy.TYPEALIAS_TYPE_REFERENCE) {
parameter<ClassId>("typealiasedClassId")
}
val ACTUAL_ANNOTATIONS_NOT_MATCH_EXPECT by warning<KtElement>(PositioningStrategy.DECLARATION_NAME_ONLY) {
parameter<Symbol>("expect")
parameter<Symbol>("actual")
}
}
val DESTRUCTING_DECLARATION by object : DiagnosticGroup("Destructuring declaration") {
@@ -628,6 +628,7 @@ object FirErrors {
val NOT_A_MULTIPLATFORM_COMPILATION by error0<PsiElement>()
val EXPECT_ACTUAL_OPT_IN_ANNOTATION by error0<KtNamedDeclaration>(SourceElementPositioningStrategies.EXPECT_ACTUAL_MODIFIER)
val ACTUAL_TYPEALIAS_TO_SPECIAL_ANNOTATION by error1<KtTypeAlias, ClassId>(SourceElementPositioningStrategies.TYPEALIAS_TYPE_REFERENCE)
val ACTUAL_ANNOTATIONS_NOT_MATCH_EXPECT by warning2<KtElement, FirBasedSymbol<*>, FirBasedSymbol<*>>(SourceElementPositioningStrategies.DECLARATION_NAME_ONLY)
// Destructuring declaration
val INITIALIZER_REQUIRED_FOR_DESTRUCTURING_DECLARATION by error0<KtDestructuringDeclaration>()
@@ -20,17 +20,20 @@ import org.jetbrains.kotlin.fir.declarations.utils.isActual
import org.jetbrains.kotlin.fir.declarations.utils.isExpect
import org.jetbrains.kotlin.fir.declarations.utils.isExternal
import org.jetbrains.kotlin.fir.declarations.utils.isTailRec
import org.jetbrains.kotlin.fir.expectActualMatchingContextFactory
import org.jetbrains.kotlin.fir.languageVersionSettings
import org.jetbrains.kotlin.fir.scopes.collectAllFunctions
import org.jetbrains.kotlin.fir.scopes.getDeclaredConstructors
import org.jetbrains.kotlin.fir.scopes.impl.declaredMemberScope
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.types.toSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirTypeAliasSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirValueParameterSymbol
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.resolve.calls.mpp.AbstractExpectActualAnnotationMatchChecker
import org.jetbrains.kotlin.resolve.checkers.OptInNames
import org.jetbrains.kotlin.resolve.multiplatform.ExpectActualCompatibility
import org.jetbrains.kotlin.resolve.multiplatform.ExpectActualCompatibility.*
@@ -176,7 +179,9 @@ object FirExpectActualDeclarationChecker : FirBasicDeclarationChecker() {
else -> {}
}
val expectedSingleCandidate = symbol.getSingleExpectForActualOrNull()
// We want to report errors even if a candidate is incompatible, but it's single
val expectedSingleCandidate = compatibilityToMembersMap[Compatible]?.singleOrNull()
?: symbol.getSingleExpectForActualOrNull()
if (expectedSingleCandidate != null) {
checkIfExpectHasDefaultArgumentsAndActualizedWithTypealias(
expectedSingleCandidate,
@@ -185,6 +190,7 @@ object FirExpectActualDeclarationChecker : FirBasicDeclarationChecker() {
reporter,
)
checkOptInAnnotation(declaration, expectedSingleCandidate, context, reporter)
checkAnnotationsMatch(expectedSingleCandidate, symbol, context, reporter)
}
}
@@ -243,6 +249,23 @@ object FirExpectActualDeclarationChecker : FirBasicDeclarationChecker() {
)
}
private fun checkAnnotationsMatch(
expectSymbol: FirBasedSymbol<*>,
actualSymbol: FirBasedSymbol<*>,
context: CheckerContext,
reporter: DiagnosticReporter
) {
val matchingContext = context.session.expectActualMatchingContextFactory.create(context.session, context.scopeSession)
val incompatibility =
AbstractExpectActualAnnotationMatchChecker.areAnnotationsCompatible(expectSymbol, actualSymbol, matchingContext) ?: return
reporter.reportOn(
actualSymbol.source, FirErrors.ACTUAL_ANNOTATIONS_NOT_MATCH_EXPECT,
incompatibility.expectSymbol as FirBasedSymbol<*>,
incompatibility.actualSymbol as FirBasedSymbol<*>,
context
)
}
fun Map<out ExpectActualCompatibility<*>, *>.allStrongIncompatibilities(): Boolean {
return keys.all { it is Incompatible && it.kind == IncompatibilityKind.STRONG }
}
@@ -178,4 +178,14 @@ object FirDiagnosticRenderers {
}
val FUNCTIONAL_TYPE_KINDS = KtDiagnosticRenderers.COLLECTION(FUNCTIONAL_TYPE_KIND)
@OptIn(SymbolInternals::class)
val SYMBOL_WITH_ANNOTATIONS = Renderer { s: FirBasedSymbol<*> ->
FirRenderer(
typeRenderer = ConeTypeRenderer(),
idRenderer = ConeIdShortRenderer(),
classMemberRenderer = null,
bodyRenderer = null,
).renderElementAsString(s.fir, trim = true)
}
}
@@ -51,6 +51,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ABSTRACT_PROPERTY
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ABSTRACT_SUPER_CALL
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ABSTRACT_SUPER_CALL_WARNING
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ACCESSOR_FOR_DELEGATED_PROPERTY
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ACTUAL_ANNOTATIONS_NOT_MATCH_EXPECT
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ACTUAL_ANNOTATION_CONFLICTING_DEFAULT_ARGUMENT_VALUE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ACTUAL_MISSING
@@ -1854,6 +1855,13 @@ object FirErrorsDefaultMessages : BaseDiagnosticRendererFactory() {
"`actual typealias` to annotation which affects code compilation can lead to incorrect behavior. Instead, use ''{0}'' annotation directly.",
TO_STRING
)
map.put(
ACTUAL_ANNOTATIONS_NOT_MATCH_EXPECT,
"All annotations from `expect` must be presented with same arguments on `actual`, otherwise they have no effect.\n" +
"Expected: {0}\n" +
"Actual: {1}",
FirDiagnosticRenderers.SYMBOL_WITH_ANNOTATIONS, FirDiagnosticRenderers.SYMBOL_WITH_ANNOTATIONS
)
// Destructuring declaration
map.put(INITIALIZER_REQUIRED_FOR_DESTRUCTURING_DECLARATION, "Initializer required for destructuring declaration")
@@ -11,12 +11,13 @@ import org.jetbrains.kotlin.descriptors.Visibility
import org.jetbrains.kotlin.fir.FirExpectActualMatchingContext
import org.jetbrains.kotlin.fir.FirExpectActualMatchingContextFactory
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
import org.jetbrains.kotlin.fir.declarations.collectEnumEntries
import org.jetbrains.kotlin.fir.declarations.isAnnotationConstructor
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.expressions.FirAnnotation
import org.jetbrains.kotlin.fir.expressions.FirConstExpression
import org.jetbrains.kotlin.fir.expressions.FirExpression
import org.jetbrains.kotlin.fir.isSubstitutionOrIntersectionOverride
import org.jetbrains.kotlin.fir.resolve.*
import org.jetbrains.kotlin.fir.resolve.dfa.coneType
import org.jetbrains.kotlin.fir.resolve.substitution.ConeSubstitutor
import org.jetbrains.kotlin.fir.scopes.*
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
@@ -26,6 +27,7 @@ import org.jetbrains.kotlin.mpp.*
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.calls.mpp.ExpectActualMatchingContext.AnnotationCallInfo
import org.jetbrains.kotlin.types.AbstractTypeChecker
import org.jetbrains.kotlin.types.TypeCheckerState
import org.jetbrains.kotlin.types.Variance
@@ -311,6 +313,48 @@ class FirExpectActualMatchingContextImpl private constructor(
override val CallableSymbolMarker.hasStableParameterNames: Boolean
get() = asSymbol().rawStatus.hasStableParameterNames
override val DeclarationSymbolMarker.annotations: List<AnnotationCallInfo>
get() = asSymbol().resolvedAnnotationsWithArguments.map(::AnnotationCallInfoImpl)
override fun areAnnotationArgumentsEqual(annotation1: AnnotationCallInfo, annotation2: AnnotationCallInfo): Boolean {
fun AnnotationCallInfo.getFirAnnotation(): FirAnnotation {
return (this as AnnotationCallInfoImpl).annotation
}
return areFirAnnotationsEqual(annotation1.getFirAnnotation(), annotation2.getFirAnnotation())
}
private fun areFirAnnotationsEqual(annotation1: FirAnnotation, annotation2: FirAnnotation): Boolean {
if (!areCompatibleExpectActualTypes(annotation1.typeRef.coneType, annotation2.typeRef.coneType)) {
return false
}
val args1 = annotation1.argumentMapping.mapping
val args2 = annotation2.argumentMapping.mapping
if (args1.size != args2.size) {
return false
}
return args1.all { (key, value1) ->
val value2 = args2[key]
value2 != null && areAnnotationArgumentsEqual(value1, value2)
}
}
private fun areAnnotationArgumentsEqual(expression1: FirExpression, expression2: FirExpression): Boolean {
// In K2 const expression calculated in backend.
// Because of that, we have "honest" checker at backend IR stage
// and "only simplest case" checker in frontend, so that we have at least some reporting in the IDE.
return when {
expression1 is FirConstExpression<*> && expression2 is FirConstExpression<*> -> {
expression1.value == expression2.value
}
else -> true
}
}
private inner class AnnotationCallInfoImpl(val annotation: FirAnnotation) : AnnotationCallInfo {
override val classId: ClassId?
get() = annotation.toAnnotationClassId(actualSession)
}
object Factory : FirExpectActualMatchingContextFactory {
override fun create(session: FirSession, scopeSession: ScopeSession): FirExpectActualMatchingContextImpl =
FirExpectActualMatchingContextImpl(session, scopeSession)