Add some validation for Experimental/UseExperimental usages
#KT-22759 In Progress
This commit is contained in:
@@ -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);
|
||||
|
||||
+7
@@ -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(
|
||||
|
||||
+91
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user