Add some validation for Experimental/UseExperimental usages

#KT-22759 In Progress
This commit is contained in:
Alexander Udalov
2017-12-20 19:43:03 +01:00
parent 0bf0a315ed
commit 6d4e8f3781
18 changed files with 281 additions and 21 deletions
@@ -229,6 +229,12 @@ public interface Errors {
DiagnosticFactory2<PsiElement, FqName, Boolean> EXPERIMENTAL_API_USAGE = DiagnosticFactory2.create(WARNING);
DiagnosticFactory2<PsiElement, FqName, Boolean> EXPERIMENTAL_API_USAGE_ERROR = DiagnosticFactory2.create(ERROR);
DiagnosticFactory0<KtAnnotationEntry> USE_EXPERIMENTAL_WITHOUT_ARGUMENTS = DiagnosticFactory0.create(WARNING);
DiagnosticFactory1<KtAnnotationEntry, FqName> USE_EXPERIMENTAL_ARGUMENT_IS_NOT_MARKER = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<KtAnnotationEntry, FqName> USE_EXPERIMENTAL_ARGUMENT_HAS_NON_COMPILATION_IMPACT = DiagnosticFactory1.create(ERROR);
DiagnosticFactory0<KtAnnotationEntry> EXPERIMENTAL_ANNOTATION_WITH_NO_IMPACT = DiagnosticFactory0.create(ERROR);
DiagnosticFactory1<KtAnnotationEntry, String> EXPERIMENTAL_ANNOTATION_WITH_WRONG_TARGET = DiagnosticFactory1.create(ERROR);
// Const
DiagnosticFactory0<PsiElement> CONST_VAL_NOT_TOP_LEVEL_OR_OBJECT = DiagnosticFactory0.create(ERROR);
DiagnosticFactory0<PsiElement> CONST_VAL_WITH_GETTER = DiagnosticFactory0.create(ERROR);
@@ -151,6 +151,13 @@ public class DefaultErrorMessages {
MAP.put(EXPERIMENTAL_API_USAGE, "This declaration is experimental and its usage should be marked with ''@{0}''{1}", TO_STRING, renderUseExperimental);
MAP.put(EXPERIMENTAL_API_USAGE_ERROR, "This declaration is experimental and its usage must be marked with ''@{0}''{1}", TO_STRING, renderUseExperimental);
MAP.put(USE_EXPERIMENTAL_WITHOUT_ARGUMENTS, "@UseExperimental without any arguments has no effect");
MAP.put(USE_EXPERIMENTAL_ARGUMENT_IS_NOT_MARKER, "Annotation ''{0}'' is not an experimental API marker, therefore its usage in @UseExperimental is ignored", TO_STRING);
MAP.put(USE_EXPERIMENTAL_ARGUMENT_HAS_NON_COMPILATION_IMPACT,
"Experimental annotation ''{0}'' has impact other than COMPILATION, therefore its usage in @UseExperimental is forbidden", TO_STRING);
MAP.put(EXPERIMENTAL_ANNOTATION_WITH_NO_IMPACT, "Experimental annotation with changesMayBreak = [] is not allowed");
MAP.put(EXPERIMENTAL_ANNOTATION_WITH_WRONG_TARGET, "Experimental annotation cannot be used on the following code elements: {0}. Please remove these targets", STRING);
MAP.put(REDUNDANT_MODIFIER, "Modifier ''{0}'' is redundant because ''{1}'' is present", TO_STRING, TO_STRING);
MAP.put(REDUNDANT_OPEN_IN_INTERFACE, "Modifier 'open' is redundant for abstract interface members");
MAP.put(REDUNDANT_MODIFIER_IN_GETTER, "Visibility modifiers are redundant in getter");
@@ -28,6 +28,7 @@ import org.jetbrains.kotlin.descriptors.annotations.KotlinRetention
import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget
import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget.*
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getAnnotationEntries
import org.jetbrains.kotlin.resolve.constants.ArrayValue
@@ -193,6 +194,8 @@ class AnnotationChecker(
}
companion object {
private val TARGET_ALLOWED_TARGETS = Name.identifier("allowedTargets")
private fun applicableTargetSet(entry: KtAnnotationEntry, trace: BindingTrace): Set<KotlinTarget> {
val descriptor = trace.get(BindingContext.ANNOTATION, entry) ?: return KotlinTarget.DEFAULT_TARGET_SET
// For descriptor with error type, all targets are considered as possible
@@ -207,10 +210,12 @@ class AnnotationChecker(
}
fun applicableTargetSet(classDescriptor: ClassDescriptor): Set<KotlinTarget>? {
val targetEntryDescriptor = classDescriptor.annotations.findAnnotation(KotlinBuiltIns.FQ_NAMES.target)
?: return null
val valueArguments = targetEntryDescriptor.allValueArguments
val valueArgument = valueArguments.entries.firstOrNull()?.value as? ArrayValue ?: return null
val targetEntryDescriptor = classDescriptor.annotations.findAnnotation(KotlinBuiltIns.FQ_NAMES.target) ?: return null
return loadAnnotationTargets(targetEntryDescriptor)
}
fun loadAnnotationTargets(targetEntryDescriptor: AnnotationDescriptor): Set<KotlinTarget>? {
val valueArgument = targetEntryDescriptor.allValueArguments[TARGET_ALLOWED_TARGETS] as? ArrayValue ?: return null
return valueArgument.value.filterIsInstance<EnumValue>().mapNotNull {
KotlinTarget.valueOrNull(it.enumEntryName.asString())
}.toSet()
@@ -98,7 +98,9 @@ private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf(
DeprecatedClassifierUsageChecker(), ApiVersionClassifierUsageChecker, MissingDependencyClassChecker.ClassifierUsage,
ExperimentalUsageChecker.ClassifierUsage
)
private val DEFAULT_ANNOTATION_CHECKERS = emptyList<AdditionalAnnotationChecker>()
private val DEFAULT_ANNOTATION_CHECKERS = listOf<AdditionalAnnotationChecker>(
ExperimentalMarkerDeclarationAnnotationChecker
)
abstract class PlatformConfigurator(
@@ -0,0 +1,91 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.resolve.checkers
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.resolve.AdditionalAnnotationChecker
import org.jetbrains.kotlin.resolve.AnnotationChecker
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingTrace
import org.jetbrains.kotlin.resolve.constants.ArrayValue
import org.jetbrains.kotlin.resolve.constants.ConstantValue
import org.jetbrains.kotlin.resolve.constants.KClassValue
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
object ExperimentalMarkerDeclarationAnnotationChecker : AdditionalAnnotationChecker {
private val WRONG_TARGETS_FOR_MARKER = setOf(KotlinTarget.EXPRESSION, KotlinTarget.FILE)
override fun checkEntries(entries: List<KtAnnotationEntry>, actualTargets: List<KotlinTarget>, trace: BindingTrace) {
var isAnnotatedWithExperimental = false
for (entry in entries) {
val annotation = trace.bindingContext.get(BindingContext.ANNOTATION, entry)
when (annotation?.fqName) {
ExperimentalUsageChecker.USE_EXPERIMENTAL_FQ_NAME -> {
val annotationClasses =
(annotation.allValueArguments[ExperimentalUsageChecker.USE_EXPERIMENTAL_ANNOTATION_CLASS] as? ArrayValue)?.value.orEmpty()
checkUseExperimentalUsage(annotationClasses, trace, entry)
}
ExperimentalUsageChecker.EXPERIMENTAL_FQ_NAME -> {
val impact = (annotation.allValueArguments[ExperimentalUsageChecker.IMPACT] as? ArrayValue)?.value
checkExperimentalUsage(impact, trace, entry)
isAnnotatedWithExperimental = true
}
}
}
if (isAnnotatedWithExperimental) {
checkMarkerTargets(entries, trace)
}
}
private fun checkUseExperimentalUsage(annotationClasses: List<ConstantValue<*>>, trace: BindingTrace, entry: KtAnnotationEntry) {
if (annotationClasses.isEmpty()) {
trace.report(Errors.USE_EXPERIMENTAL_WITHOUT_ARGUMENTS.on(entry))
return
}
for (annotationClass in annotationClasses) {
val classDescriptor =
(annotationClass as? KClassValue)?.value?.constructor?.declarationDescriptor as? ClassDescriptor
?: continue
val experimentality = with(ExperimentalUsageChecker) {
classDescriptor.loadExperimentalityForMarkerAnnotation()
}
if (experimentality == null) {
trace.report(Errors.USE_EXPERIMENTAL_ARGUMENT_IS_NOT_MARKER.on(entry, classDescriptor.fqNameSafe))
} else if (!experimentality.isCompilationOnly) {
trace.report(Errors.USE_EXPERIMENTAL_ARGUMENT_HAS_NON_COMPILATION_IMPACT.on(entry, experimentality.annotationFqName))
}
}
}
private fun checkExperimentalUsage(impact: List<ConstantValue<*>>?, trace: BindingTrace, entry: KtAnnotationEntry) {
if (impact != null && impact.isEmpty()) {
trace.report(Errors.EXPERIMENTAL_ANNOTATION_WITH_NO_IMPACT.on(entry))
}
}
private fun checkMarkerTargets(entries: List<KtAnnotationEntry>, trace: BindingTrace) {
val targetEntry =
entries.associate { entry -> entry to trace.bindingContext.get(BindingContext.ANNOTATION, entry) }
.entries
.firstOrNull { (_, descriptor) -> descriptor != null && descriptor.fqName == KotlinBuiltIns.FQ_NAMES.target }
?: return
val (entry, descriptor) = targetEntry
val allowedTargets = AnnotationChecker.loadAnnotationTargets(descriptor!!) ?: return
val wrongTargets = allowedTargets.intersect(WRONG_TARGETS_FOR_MARKER)
if (wrongTargets.isNotEmpty()) {
trace.report(
Errors.EXPERIMENTAL_ANNOTATION_WITH_WRONG_TARGET.on(entry, wrongTargets.joinToString(transform = KotlinTarget::description))
)
}
}
}
@@ -41,20 +41,20 @@ import org.jetbrains.kotlin.utils.SmartSet
import org.jetbrains.kotlin.utils.addIfNotNull
object ExperimentalUsageChecker : CallChecker {
private val EXPERIMENTAL_FQ_NAME = FqName("kotlin.Experimental")
private val USE_EXPERIMENTAL_FQ_NAME = FqName("kotlin.UseExperimental")
private val USE_EXPERIMENTAL_ANNOTATION_CLASS = Name.identifier("markerClass")
internal val EXPERIMENTAL_FQ_NAME = FqName("kotlin.Experimental")
internal val USE_EXPERIMENTAL_FQ_NAME = FqName("kotlin.UseExperimental")
internal val USE_EXPERIMENTAL_ANNOTATION_CLASS = Name.identifier("markerClass")
private val LEVEL = Name.identifier("level")
private val WARNING_LEVEL = Name.identifier("WARNING")
private val ERROR_LEVEL = Name.identifier("ERROR")
private val IMPACT = Name.identifier("changesMayBreak")
internal val IMPACT = Name.identifier("changesMayBreak")
private val COMPILATION_IMPACT = Name.identifier("COMPILATION")
private val LINKAGE_IMPACT = Name.identifier("LINKAGE")
private val RUNTIME_IMPACT = Name.identifier("RUNTIME")
private data class Experimentality(val annotationFqName: FqName, val severity: Severity, val impact: List<Impact>) {
internal data class Experimentality(val annotationFqName: FqName, val severity: Severity, private val impact: List<Impact>) {
val isCompilationOnly: Boolean get() = impact.all(Impact.COMPILATION::equals)
enum class Severity { WARNING, ERROR }
@@ -72,7 +72,7 @@ object ExperimentalUsageChecker : CallChecker {
private fun checkExperimental(descriptor: DeclarationDescriptor, element: PsiElement, trace: BindingTrace) {
for (experimentality in descriptor.loadExperimentalities()) {
val (annotationFqName, severity, impact) = experimentality
val (annotationFqName, severity) = experimentality
val isBodyUsageOfCompilationExperimentality =
experimentality.isCompilationOnly && element.isBodyUsage()
@@ -95,22 +95,21 @@ object ExperimentalUsageChecker : CallChecker {
val result = SmartSet.create<Experimentality>()
for (annotation in annotations) {
result.addIfNotNull(annotation.loadExperimentalityForMarkerAnnotation())
result.addIfNotNull(annotation.annotationClass?.loadExperimentalityForMarkerAnnotation())
}
val container = containingDeclaration
if (container is ClassDescriptor && this !is ConstructorDescriptor) {
for (annotation in container.annotations) {
result.addIfNotNull(annotation.loadExperimentalityForMarkerAnnotation())
result.addIfNotNull(annotation.annotationClass?.loadExperimentalityForMarkerAnnotation())
}
}
return result
}
private fun AnnotationDescriptor.loadExperimentalityForMarkerAnnotation(): Experimentality? {
val experimental = annotationClass?.annotations?.findAnnotation(EXPERIMENTAL_FQ_NAME) ?: return null
val annotationFqName = fqName ?: return null
internal fun ClassDescriptor.loadExperimentalityForMarkerAnnotation(): Experimentality? {
val experimental = annotations.findAnnotation(EXPERIMENTAL_FQ_NAME) ?: return null
val severity = when ((experimental.allValueArguments[LEVEL] as? EnumValue)?.enumEntryName) {
WARNING_LEVEL -> Experimentality.Severity.WARNING
@@ -126,7 +125,7 @@ object ExperimentalUsageChecker : CallChecker {
}
} ?: Experimentality.DEFAULT_IMPACT
return Experimentality(annotationFqName, severity, impact)
return Experimentality(fqNameSafe, severity, impact)
}
// Returns true if this element appears in the body of some function and is not visible in any non-local declaration signature.
@@ -189,4 +188,5 @@ object ExperimentalUsageChecker : CallChecker {
checkExperimental(targetDescriptor, element, context.trace)
}
}
}