Support -Xexperimental/-Xuse-experimental in ExperimentalUsageChecker

#KT-22759 In Progress
This commit is contained in:
Alexander Udalov
2018-01-17 16:43:02 +01:00
parent 77625831f7
commit 5e78adb501
8 changed files with 165 additions and 24 deletions
@@ -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<Experimentality>,
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
@@ -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()
}
@@ -0,0 +1,24 @@
// -- Module: <api> --
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: <usage> --
package
package api {
}
package usage {
public fun use(): kotlin.Unit
}
@@ -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()
}
@@ -0,0 +1,24 @@
// -- Module: <api> --
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: <usage> --
package
package api {
}
package usage {
public fun use(): kotlin.Unit
}
@@ -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<LanguageFeature, LanguageFeature.State>,
override val apiVersion: ApiVersion,
@@ -51,15 +54,21 @@ private fun specificFeaturesForTests(): Map<LanguageFeature, LanguageFeature.Sta
fun parseLanguageVersionSettings(directiveMap: Map<String, String>): 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) {
@@ -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");
@@ -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");