Introduce Experimental and UseExperimental annotations
#KT-22759 In Progress
This commit is contained in:
@@ -226,6 +226,9 @@ public interface Errors {
|
||||
DiagnosticFactory1<PsiElement, FqName> ILLEGAL_KOTLIN_VERSION_STRING_VALUE = DiagnosticFactory1.create(ERROR);
|
||||
DiagnosticFactory1<PsiElement, String> NEWER_VERSION_IN_SINCE_KOTLIN = DiagnosticFactory1.create(WARNING);
|
||||
|
||||
DiagnosticFactory2<PsiElement, FqName, Boolean> EXPERIMENTAL_API_USAGE = DiagnosticFactory2.create(WARNING);
|
||||
DiagnosticFactory2<PsiElement, FqName, Boolean> EXPERIMENTAL_API_USAGE_ERROR = DiagnosticFactory2.create(ERROR);
|
||||
|
||||
// Const
|
||||
DiagnosticFactory0<PsiElement> CONST_VAL_NOT_TOP_LEVEL_OR_OBJECT = DiagnosticFactory0.create(ERROR);
|
||||
DiagnosticFactory0<PsiElement> CONST_VAL_WITH_GETTER = DiagnosticFactory0.create(ERROR);
|
||||
|
||||
+13
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.config.LanguageVersion;
|
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic;
|
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory;
|
||||
import org.jetbrains.kotlin.diagnostics.Errors;
|
||||
import org.jetbrains.kotlin.name.FqName;
|
||||
import org.jetbrains.kotlin.resolve.VarianceConflictDiagnosticData;
|
||||
import org.jetbrains.kotlin.serialization.deserialization.descriptors.VersionRequirement;
|
||||
import org.jetbrains.kotlin.types.KotlinTypeKt;
|
||||
@@ -22,6 +23,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.AddToStdlibKt;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ServiceLoader;
|
||||
@@ -138,6 +140,17 @@ public class DefaultErrorMessages {
|
||||
MAP.put(ILLEGAL_KOTLIN_VERSION_STRING_VALUE, "Invalid @{0} annotation value (should be ''major.minor'' or ''major.minor.patch'')", TO_STRING);
|
||||
MAP.put(NEWER_VERSION_IN_SINCE_KOTLIN, "The version is greater than the specified API version {0}", STRING);
|
||||
|
||||
RenderingContext.Key<FqName> getExperimentalFqName = new RenderingContext.Key<FqName>("getExperimentalFqName") {
|
||||
@Override
|
||||
public FqName compute(@NotNull Collection<?> objectsToRender) {
|
||||
return (FqName) CollectionsKt.first(objectsToRender);
|
||||
}
|
||||
};
|
||||
DiagnosticParameterRenderer<Boolean> renderUseExperimental = (useExperimentalAllowed, c) ->
|
||||
useExperimentalAllowed ? " or ''@UseExperimental(" + c.get(getExperimentalFqName) + "::class)''" : "";
|
||||
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(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");
|
||||
|
||||
+3
-8
@@ -29,17 +29,12 @@ sealed class RenderingContext {
|
||||
class Impl(private val objectsToRender: Collection<Any?>) : RenderingContext() {
|
||||
private val data = linkedMapOf<Key<*>, Any?>()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> get(key: Key<T>): T {
|
||||
if (!data.containsKey(key)) {
|
||||
val result = key.compute(objectsToRender)
|
||||
data[key] = result
|
||||
return result
|
||||
}
|
||||
return data[key] as T
|
||||
return data[key] as? T ?: key.compute(objectsToRender).also { data[key] = it }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object Empty : RenderingContext() {
|
||||
override fun <T> get(key: Key<T>): T {
|
||||
return key.compute(emptyList())
|
||||
@@ -65,4 +60,4 @@ sealed class RenderingContext {
|
||||
return Impl(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +91,14 @@ private val DEFAULT_CALL_CHECKERS = listOf(
|
||||
CoroutineSuspendCallChecker, BuilderFunctionsCallChecker, DslScopeViolationCallChecker, MissingDependencyClassChecker,
|
||||
CallableReferenceCompatibilityChecker(), LateinitIntrinsicApplicabilityChecker,
|
||||
UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker(),
|
||||
PrimitiveNumericComparisonCallChecker, LambdaWithSuspendModifierCallChecker
|
||||
PrimitiveNumericComparisonCallChecker, LambdaWithSuspendModifierCallChecker, ExperimentalUsageChecker
|
||||
)
|
||||
private val DEFAULT_TYPE_CHECKERS = emptyList<AdditionalTypeChecker>()
|
||||
private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf(
|
||||
DeprecatedClassifierUsageChecker(), ApiVersionClassifierUsageChecker, MissingDependencyClassChecker.ClassifierUsage
|
||||
DeprecatedClassifierUsageChecker(), ApiVersionClassifierUsageChecker, MissingDependencyClassChecker.ClassifierUsage,
|
||||
ExperimentalUsageChecker.ClassifierUsage
|
||||
)
|
||||
private val DEFAULT_ANNOTATION_CHECKERS = emptyList<AdditionalAnnotationChecker>()
|
||||
|
||||
|
||||
abstract class PlatformConfigurator(
|
||||
@@ -105,7 +107,7 @@ abstract class PlatformConfigurator(
|
||||
additionalCallCheckers: List<CallChecker>,
|
||||
additionalTypeCheckers: List<AdditionalTypeChecker>,
|
||||
additionalClassifierUsageCheckers: List<ClassifierUsageChecker>,
|
||||
private val additionalAnnotationCheckers: List<AdditionalAnnotationChecker>,
|
||||
additionalAnnotationCheckers: List<AdditionalAnnotationChecker>,
|
||||
private val identifierChecker: IdentifierChecker,
|
||||
private val overloadFilter: OverloadFilter,
|
||||
private val platformToKotlinClassMap: PlatformToKotlinClassMap,
|
||||
@@ -118,6 +120,7 @@ abstract class PlatformConfigurator(
|
||||
private val typeCheckers: List<AdditionalTypeChecker> = DEFAULT_TYPE_CHECKERS + additionalTypeCheckers
|
||||
private val classifierUsageCheckers: List<ClassifierUsageChecker> =
|
||||
DEFAULT_CLASSIFIER_USAGE_CHECKERS + additionalClassifierUsageCheckers
|
||||
private val annotationCheckers: List<AdditionalAnnotationChecker> = DEFAULT_ANNOTATION_CHECKERS + additionalAnnotationCheckers
|
||||
|
||||
abstract fun configureModuleComponents(container: StorageComponentContainer)
|
||||
|
||||
@@ -127,7 +130,7 @@ abstract class PlatformConfigurator(
|
||||
callCheckers.forEach { useInstance(it) }
|
||||
typeCheckers.forEach { useInstance(it) }
|
||||
classifierUsageCheckers.forEach { useInstance(it) }
|
||||
additionalAnnotationCheckers.forEach { useInstance(it) }
|
||||
annotationCheckers.forEach { useInstance(it) }
|
||||
useInstance(identifierChecker)
|
||||
useInstance(overloadFilter)
|
||||
useInstance(platformToKotlinClassMap)
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2010-2017 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.resolve.checkers
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ClassifierDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
|
||||
import org.jetbrains.kotlin.diagnostics.Errors
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.BindingTrace
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
|
||||
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.constants.ArrayValue
|
||||
import org.jetbrains.kotlin.resolve.constants.EnumValue
|
||||
import org.jetbrains.kotlin.resolve.constants.KClassValue
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.annotationClass
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
||||
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")
|
||||
|
||||
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")
|
||||
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>) {
|
||||
val isCompilationOnly: Boolean get() = impact.all(Impact.COMPILATION::equals)
|
||||
|
||||
enum class Severity { WARNING, ERROR }
|
||||
enum class Impact { COMPILATION, LINKAGE_OR_RUNTIME }
|
||||
|
||||
companion object {
|
||||
val DEFAULT_SEVERITY = Severity.ERROR
|
||||
val DEFAULT_IMPACT = listOf(Impact.COMPILATION, Impact.LINKAGE_OR_RUNTIME)
|
||||
}
|
||||
}
|
||||
|
||||
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
|
||||
checkExperimental(resolvedCall.resultingDescriptor, reportOn, context.trace)
|
||||
}
|
||||
|
||||
private fun checkExperimental(descriptor: DeclarationDescriptor, element: PsiElement, trace: BindingTrace) {
|
||||
for (experimentality in descriptor.loadExperimentalities()) {
|
||||
val (annotationFqName, severity, impact) = experimentality
|
||||
val isBodyUsageOfCompilationExperimentality =
|
||||
experimentality.isCompilationOnly && element.isBodyUsage()
|
||||
|
||||
val isExperimentalityAccepted =
|
||||
(isBodyUsageOfCompilationExperimentality &&
|
||||
element.hasContainerAnnotatedWithUseExperimental(annotationFqName, trace.bindingContext)) ||
|
||||
element.propagates(annotationFqName, trace.bindingContext)
|
||||
|
||||
if (!isExperimentalityAccepted) {
|
||||
val diagnostic = when (severity) {
|
||||
ExperimentalUsageChecker.Experimentality.Severity.WARNING -> Errors.EXPERIMENTAL_API_USAGE
|
||||
ExperimentalUsageChecker.Experimentality.Severity.ERROR -> Errors.EXPERIMENTAL_API_USAGE_ERROR
|
||||
}
|
||||
trace.report(diagnostic.on(element, annotationFqName, isBodyUsageOfCompilationExperimentality))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DeclarationDescriptor.loadExperimentalities(): Set<Experimentality> {
|
||||
val result = SmartSet.create<Experimentality>()
|
||||
|
||||
for (annotation in annotations) {
|
||||
result.addIfNotNull(annotation.loadExperimentalityForMarkerAnnotation())
|
||||
}
|
||||
|
||||
val container = containingDeclaration
|
||||
if (container is ClassDescriptor && this !is ConstructorDescriptor) {
|
||||
for (annotation in container.annotations) {
|
||||
result.addIfNotNull(annotation.loadExperimentalityForMarkerAnnotation())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun AnnotationDescriptor.loadExperimentalityForMarkerAnnotation(): Experimentality? {
|
||||
val experimental = annotationClass?.annotations?.findAnnotation(EXPERIMENTAL_FQ_NAME) ?: return null
|
||||
val annotationFqName = fqName ?: return null
|
||||
|
||||
val severity = when ((experimental.allValueArguments[LEVEL] as? EnumValue)?.enumEntryName) {
|
||||
WARNING_LEVEL -> Experimentality.Severity.WARNING
|
||||
ERROR_LEVEL -> Experimentality.Severity.ERROR
|
||||
else -> Experimentality.DEFAULT_SEVERITY
|
||||
}
|
||||
|
||||
val impact = (experimental.allValueArguments[IMPACT] as? ArrayValue)?.value?.mapNotNull { impact ->
|
||||
when ((impact as? EnumValue)?.enumEntryName) {
|
||||
COMPILATION_IMPACT -> Experimentality.Impact.COMPILATION
|
||||
LINKAGE_IMPACT, RUNTIME_IMPACT -> Experimentality.Impact.LINKAGE_OR_RUNTIME
|
||||
else -> null
|
||||
}
|
||||
} ?: Experimentality.DEFAULT_IMPACT
|
||||
|
||||
return Experimentality(annotationFqName, severity, impact)
|
||||
}
|
||||
|
||||
// Returns true if this element appears in the body of some function and is not visible in any non-local declaration signature.
|
||||
// If that's the case, one can opt-in to using the corresponding experimental API by annotating this element (or any of its
|
||||
// enclosing declarations) with @UseExperimental(X::class), not requiring propagation of the experimental annotation to the call sites.
|
||||
// (Note that this is allowed only if X's impact is [COMPILATION].)
|
||||
private fun PsiElement.isBodyUsage(): Boolean {
|
||||
return anyParentMatches { element, parent ->
|
||||
element == (parent as? KtDeclarationWithBody)?.bodyExpression ||
|
||||
element == (parent as? KtDeclarationWithInitializer)?.initializer ||
|
||||
element == (parent as? KtClassInitializer)?.body ||
|
||||
element == (parent as? KtParameter)?.defaultValue ||
|
||||
element == (parent as? KtSuperTypeCallEntry)?.valueArgumentList ||
|
||||
element == (parent as? KtDelegatedSuperTypeEntry)?.delegateExpression ||
|
||||
element == (parent as? KtPropertyDelegate)?.expression
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 inline fun PsiElement.anyParentMatches(predicate: (element: PsiElement, parent: PsiElement?) -> Boolean): Boolean {
|
||||
var element = this
|
||||
while (true) {
|
||||
val parent = element.parent
|
||||
if (predicate(element, parent)) return true
|
||||
element = parent ?: return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnnotationDescriptor.isUseExperimental(annotationFqName: FqName): Boolean {
|
||||
if (fqName != USE_EXPERIMENTAL_FQ_NAME) return false
|
||||
|
||||
val annotationClasses = allValueArguments[USE_EXPERIMENTAL_ANNOTATION_CLASS]
|
||||
return annotationClasses is ArrayValue && annotationClasses.value.any { annotationClass ->
|
||||
(annotationClass as? KClassValue)?.value?.constructor?.declarationDescriptor?.fqNameSafe == annotationFqName
|
||||
}
|
||||
}
|
||||
|
||||
object ClassifierUsage : ClassifierUsageChecker {
|
||||
override fun check(targetDescriptor: ClassifierDescriptor, element: PsiElement, context: ClassifierUsageCheckerContext) {
|
||||
checkExperimental(targetDescriptor, element, context.trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user