From 5e78adb50157dfda3cc00e23baa19c2d63e9b46b Mon Sep 17 00:00:00 2001 From: Alexander Udalov Date: Wed, 17 Jan 2018 16:43:02 +0100 Subject: [PATCH] Support -Xexperimental/-Xuse-experimental in ExperimentalUsageChecker #KT-22759 In Progress --- .../checkers/ExperimentalUsageChecker.kt | 54 ++++++++++++------- .../experimental/experimentalOnWholeModule.kt | 23 ++++++++ .../experimentalOnWholeModule.txt | 24 +++++++++ .../useExperimentalOnWholeModule.kt | 23 ++++++++ .../useExperimentalOnWholeModule.txt | 24 +++++++++ .../CompilerTestLanguageVersionSettings.kt | 17 ++++-- .../DiagnosticsTestWithStdLibGenerated.java | 12 +++++ ...ticsTestWithStdLibUsingJavacGenerated.java | 12 +++++ 8 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.kt create mode 100644 compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.txt create mode 100644 compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.kt create mode 100644 compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.txt diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExperimentalUsageChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExperimentalUsageChecker.kt index 0be34758d80..87fc8c8240c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExperimentalUsageChecker.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExperimentalUsageChecker.kt @@ -78,8 +78,10 @@ object ExperimentalUsageChecker : CallChecker { private fun checkExperimental(descriptor: DeclarationDescriptor, element: PsiElement, context: CheckerContext) { val experimentalities = descriptor.loadExperimentalities() if (experimentalities.isNotEmpty()) { - checkExperimental(experimentalities, element, context.trace.bindingContext, context.moduleDescriptor) { - experimentality, isBodyUsageOfSourceOnlyExperimentality -> + checkExperimental( + experimentalities, element, context.trace.bindingContext, context.languageVersionSettings, + context.moduleDescriptor + ) { experimentality, isBodyUsageOfSourceOnlyExperimentality -> val diagnostic = when (experimentality.severity) { Experimentality.Severity.WARNING -> Errors.EXPERIMENTAL_API_USAGE Experimentality.Severity.ERROR -> Errors.EXPERIMENTAL_API_USAGE_ERROR @@ -93,6 +95,7 @@ object ExperimentalUsageChecker : CallChecker { experimentalities: Collection, element: PsiElement, bindingContext: BindingContext, + languageVersionSettings: LanguageVersionSettings, module: ModuleDescriptor, report: (experimentality: Experimentality, isBodyUsageOfCompilationExperimentality: Boolean) -> Unit ) { @@ -106,11 +109,12 @@ object ExperimentalUsageChecker : CallChecker { val isBodyUsageInSameModule = experimentality.markerDescriptor.module == module && isBodyUsageExceptPublicInline + val annotationFqName = experimentality.annotationFqName val isExperimentalityAccepted = isBodyUsageInSameModule || (isBodyUsageOfCompilationExperimentality && - element.hasContainerAnnotatedWithUseExperimental(experimentality.annotationFqName, bindingContext)) || - element.propagates(experimentality.annotationFqName, bindingContext) + element.hasContainerAnnotatedWithUseExperimental(annotationFqName, bindingContext, languageVersionSettings)) || + element.propagates(annotationFqName, bindingContext, languageVersionSettings) if (!isExperimentalityAccepted) { report(experimentality, isBodyUsageOfCompilationExperimentality) @@ -188,24 +192,32 @@ object ExperimentalUsageChecker : CallChecker { // Checks whether any of the non-local enclosing declarations is annotated with annotationFqName, effectively requiring // propagation for the experimental annotation to the call sites - private fun PsiElement.propagates(annotationFqName: FqName, bindingContext: BindingContext): Boolean { - return anyParentMatches { element, _ -> - if (element is KtDeclaration) { - val descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, element) - descriptor != null && !DescriptorUtils.isLocal(descriptor) && descriptor.annotations.hasAnnotation(annotationFqName) - } else false - } - } + private fun PsiElement.propagates( + annotationFqName: FqName, + bindingContext: BindingContext, + languageVersionSettings: LanguageVersionSettings + ): Boolean = + annotationFqName.asString() in languageVersionSettings.getFlag(AnalysisFlag.experimental) || + anyParentMatches { element, _ -> + if (element is KtDeclaration) { + val descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, element) + descriptor != null && !DescriptorUtils.isLocal(descriptor) && descriptor.annotations.hasAnnotation(annotationFqName) + } else false + } // Checks whether there's an element lexically above the tree, that is annotated with `@UseExperimental(X::class)` // where annotationFqName is the FQ name of X - private fun PsiElement.hasContainerAnnotatedWithUseExperimental(annotationFqName: FqName, bindingContext: BindingContext): Boolean { - return anyParentMatches { element, _ -> - element is KtAnnotated && element.annotationEntries.any { entry -> - bindingContext.get(BindingContext.ANNOTATION, entry)?.isUseExperimental(annotationFqName) == true - } - } - } + private fun PsiElement.hasContainerAnnotatedWithUseExperimental( + annotationFqName: FqName, + bindingContext: BindingContext, + languageVersionSettings: LanguageVersionSettings + ): Boolean = + annotationFqName.asString() in languageVersionSettings.getFlag(AnalysisFlag.useExperimental) || + anyParentMatches { element, _ -> + element is KtAnnotated && element.annotationEntries.any { entry -> + bindingContext.get(BindingContext.ANNOTATION, entry)?.isUseExperimental(annotationFqName) == true + } + } private inline fun PsiElement.anyParentMatches(predicate: (element: PsiElement, parent: PsiElement?) -> Boolean): Boolean { var element = this @@ -273,7 +285,9 @@ object ExperimentalUsageChecker : CallChecker { val module = descriptor.module for ((experimentality, member) in experimentalOverridden) { - checkExperimental(listOf(experimentality), declaration, context.trace.bindingContext, module) { _, _ -> + checkExperimental( + listOf(experimentality), declaration, context.trace.bindingContext, context.languageVersionSettings, module + ) { _, _ -> val diagnostic = when (experimentality.severity) { Experimentality.Severity.WARNING -> Errors.EXPERIMENTAL_OVERRIDE Experimentality.Severity.ERROR -> Errors.EXPERIMENTAL_OVERRIDE_ERROR diff --git a/compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.kt b/compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.kt new file mode 100644 index 00000000000..c5c23adb144 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.kt @@ -0,0 +1,23 @@ +// !API_VERSION: 1.3 +// !EXPERIMENTAL: api.ExperimentalAPI +// MODULE: api +// FILE: api.kt + +package api + +@Experimental +annotation class ExperimentalAPI + +@ExperimentalAPI +fun function(): String = "" + +// MODULE: usage(api) +// FILE: usage.kt + +package usage + +import api.* + +fun use() { + function() +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.txt b/compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.txt new file mode 100644 index 00000000000..e04b8b105ef --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.txt @@ -0,0 +1,24 @@ +// -- Module: -- +package + +package api { + @api.ExperimentalAPI public fun function(): kotlin.String + + @kotlin.Experimental public final annotation class ExperimentalAPI : kotlin.Annotation { + public constructor ExperimentalAPI() + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } +} + + +// -- Module: -- +package + +package api { +} + +package usage { + public fun use(): kotlin.Unit +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.kt b/compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.kt new file mode 100644 index 00000000000..36f4e1eebe1 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.kt @@ -0,0 +1,23 @@ +// !API_VERSION: 1.3 +// !USE_EXPERIMENTAL: api.ExperimentalAPI +// MODULE: api +// FILE: api.kt + +package api + +@Experimental(Experimental.Level.ERROR, [Experimental.Impact.COMPILATION]) +annotation class ExperimentalAPI + +@ExperimentalAPI +fun function(): String = "" + +// MODULE: usage(api) +// FILE: usage.kt + +package usage + +import api.* + +fun use() { + function() +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.txt b/compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.txt new file mode 100644 index 00000000000..d1862fe539f --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.txt @@ -0,0 +1,24 @@ +// -- Module: -- +package + +package api { + @api.ExperimentalAPI public fun function(): kotlin.String + + @kotlin.Experimental(changesMayBreak = {Impact.COMPILATION}, level = Level.ERROR) public final annotation class ExperimentalAPI : kotlin.Annotation { + public constructor ExperimentalAPI() + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } +} + + +// -- Module: -- +package + +package api { +} + +package usage { + public fun use(): kotlin.Unit +} diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/CompilerTestLanguageVersionSettings.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/CompilerTestLanguageVersionSettings.kt index 78c50ef2324..8f2169e1145 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/CompilerTestLanguageVersionSettings.kt +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/CompilerTestLanguageVersionSettings.kt @@ -26,6 +26,9 @@ import java.util.regex.Pattern const val LANGUAGE_DIRECTIVE = "LANGUAGE" const val API_VERSION_DIRECTIVE = "API_VERSION" +const val EXPERIMENTAL_DIRECTIVE = "EXPERIMENTAL" +const val USE_EXPERIMENTAL_DIRECTIVE = "USE_EXPERIMENTAL" + data class CompilerTestLanguageVersionSettings( private val initialLanguageFeatures: Map, override val apiVersion: ApiVersion, @@ -51,15 +54,21 @@ private fun specificFeaturesForTests(): Map): LanguageVersionSettings? { val apiVersionString = directiveMap[API_VERSION_DIRECTIVE] - val directives = directiveMap[LANGUAGE_DIRECTIVE] - if (apiVersionString == null && directives == null) return null + val languageFeaturesString = directiveMap[LANGUAGE_DIRECTIVE] + val experimental = directiveMap[EXPERIMENTAL_DIRECTIVE]?.split(' ')?.let { AnalysisFlag.experimental to it } + val useExperimental = directiveMap[USE_EXPERIMENTAL_DIRECTIVE]?.split(' ')?.let { AnalysisFlag.useExperimental to it } + + if (apiVersionString == null && languageFeaturesString == null && experimental == null && useExperimental == null) return null val apiVersion = (if (apiVersionString != null) ApiVersion.parse(apiVersionString) else ApiVersion.LATEST_STABLE) ?: error("Unknown API version: $apiVersionString") - val languageFeatures = directives?.let(::collectLanguageFeatureMap).orEmpty() + val languageFeatures = languageFeaturesString?.let(::collectLanguageFeatureMap).orEmpty() - return CompilerTestLanguageVersionSettings(languageFeatures, apiVersion, LanguageVersion.LATEST_STABLE) + return CompilerTestLanguageVersionSettings( + languageFeatures, apiVersion, LanguageVersion.LATEST_STABLE, + mapOf(*listOfNotNull(experimental, useExperimental).toTypedArray()) + ) } fun setupLanguageVersionSettingsForCompilerTests(originalFileText: String, environment: KotlinCoreEnvironment) { diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java index 8469edc41eb..3562bc5b42f 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java @@ -1845,6 +1845,12 @@ public class DiagnosticsTestWithStdLibGenerated extends AbstractDiagnosticsTestW doTest(fileName); } + @TestMetadata("experimentalOnWholeModule.kt") + public void testExperimentalOnWholeModule() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.kt"); + doTest(fileName); + } + @TestMetadata("experimentalWithNoImpact.kt") public void testExperimentalWithNoImpact() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalWithNoImpact.kt"); @@ -1899,6 +1905,12 @@ public class DiagnosticsTestWithStdLibGenerated extends AbstractDiagnosticsTestW doTest(fileName); } + @TestMetadata("useExperimentalOnWholeModule.kt") + public void testUseExperimentalOnWholeModule() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.kt"); + doTest(fileName); + } + @TestMetadata("useExperimentalTargets.kt") public void testUseExperimentalTargets() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalTargets.kt"); diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java index 19cba297f4b..e5d3b0e37bc 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java @@ -1845,6 +1845,12 @@ public class DiagnosticsTestWithStdLibUsingJavacGenerated extends AbstractDiagno doTest(fileName); } + @TestMetadata("experimentalOnWholeModule.kt") + public void testExperimentalOnWholeModule() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalOnWholeModule.kt"); + doTest(fileName); + } + @TestMetadata("experimentalWithNoImpact.kt") public void testExperimentalWithNoImpact() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalWithNoImpact.kt"); @@ -1899,6 +1905,12 @@ public class DiagnosticsTestWithStdLibUsingJavacGenerated extends AbstractDiagno doTest(fileName); } + @TestMetadata("useExperimentalOnWholeModule.kt") + public void testUseExperimentalOnWholeModule() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalOnWholeModule.kt"); + doTest(fileName); + } + @TestMetadata("useExperimentalTargets.kt") public void testUseExperimentalTargets() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/useExperimentalTargets.kt");