Load module annotations for JVM and JS modules in compiler

#KT-22759 In Progress
This commit is contained in:
Alexander Udalov
2018-01-23 13:33:47 +01:00
parent 396a5ab352
commit 890374a42a
21 changed files with 478 additions and 211 deletions
@@ -0,0 +1,28 @@
/*
* 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.cli.jvm.compiler
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.PackagePartProvider
import org.jetbrains.kotlin.js.resolve.getAnnotationsOnContainingJsModule
import org.jetbrains.kotlin.load.kotlin.getJvmModuleNameForDeserializedDescriptor
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.ModuleAnnotationsResolver
class CliModuleAnnotationsResolver : ModuleAnnotationsResolver {
private val packagePartProviders = mutableListOf<PackagePartProvider>()
fun addPackagePartProvider(packagePartProvider: PackagePartProvider) {
packagePartProviders += packagePartProvider
}
override fun getAnnotationsOnContainingModule(descriptor: DeclarationDescriptor): List<ClassId> {
getAnnotationsOnContainingJsModule(descriptor)?.let { return it }
val moduleName = getJvmModuleNameForDeserializedDescriptor(descriptor) ?: return emptyList()
return packagePartProviders.flatMap { it.getAnnotationsOnBinaryModule(moduleName) }
}
}
@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.descriptors.PackagePartProvider
import org.jetbrains.kotlin.load.kotlin.ModuleMapping
import org.jetbrains.kotlin.load.kotlin.PackageParts
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.CompilerDeserializationConfiguration
import java.io.EOFException
@@ -31,7 +32,7 @@ class JvmPackagePartProvider(
languageVersionSettings: LanguageVersionSettings,
private val scope: GlobalSearchScope
) : PackagePartProvider {
private data class ModuleMappingInfo(val root: VirtualFile, val mapping: ModuleMapping)
private data class ModuleMappingInfo(val root: VirtualFile, val mapping: ModuleMapping, val name: String)
private val deserializationConfiguration = CompilerDeserializationConfiguration(languageVersionSettings)
@@ -68,6 +69,12 @@ class JvmPackagePartProvider(
return result
}
override fun getAnnotationsOnBinaryModule(moduleName: String): List<ClassId> {
return loadedModules.mapNotNull { (_, mapping, name) ->
if (name == moduleName) mapping.moduleData.annotations else null
}.flatten()
}
fun addRoots(roots: List<JavaRoot>) {
for ((root, type) in roots) {
if (type != JavaRoot.RootType.BINARY) continue
@@ -83,7 +90,7 @@ class JvmPackagePartProvider(
catch (e: EOFException) {
throw RuntimeException("Error on reading package parts from $moduleFile in $root", e)
}
loadedModules.add(ModuleMappingInfo(root, mapping))
loadedModules.add(ModuleMappingInfo(root, mapping, moduleFile.nameWithoutExtension))
}
}
}
@@ -29,8 +29,6 @@ import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.lang.MetaLanguage
import com.intellij.lang.java.JavaParserDefinition
import com.intellij.lang.jvm.facade.JvmElementProvider
import com.intellij.lang.jvm.facade.JvmFacade
import com.intellij.lang.jvm.facade.JvmFacadeImpl
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.application.TransactionGuardImpl
@@ -101,6 +99,7 @@ import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory
import org.jetbrains.kotlin.parsing.KotlinParserDefinition
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.CodeAnalyzerInitializer
import org.jetbrains.kotlin.resolve.ModuleAnnotationsResolver
import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension
import org.jetbrains.kotlin.resolve.jvm.KotlinJavaPsiFacade
import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
@@ -277,6 +276,7 @@ class KotlinCoreEnvironment private constructor(
return JvmPackagePartProvider(configuration.languageVersionSettings, scope).apply {
addRoots(initialRoots)
packagePartProviders += this
(ModuleAnnotationsResolver.getInstance(project) as CliModuleAnnotationsResolver).addPackagePartProvider(this)
}
}
@@ -569,6 +569,7 @@ class KotlinCoreEnvironment private constructor(
registerService(ScriptDependenciesProvider::class.java, CliScriptDependenciesProvider(projectEnvironment.project, scriptDefinitionProvider))
registerService(KotlinJavaPsiFacade::class.java, KotlinJavaPsiFacade(this))
registerService(KtLightClassForFacade.FacadeStubCache::class.java, KtLightClassForFacade.FacadeStubCache(this))
registerService(ModuleAnnotationsResolver::class.java, CliModuleAnnotationsResolver())
if (messageCollector != null) {
registerService(ScriptReportSink::class.java, CliScriptReportSink(messageCollector))
}
@@ -19,6 +19,7 @@ package org.jetbrains.kotlin.load.kotlin.incremental
import org.jetbrains.kotlin.descriptors.PackagePartProvider
import org.jetbrains.kotlin.load.kotlin.ModuleMapping
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.serialization.deserialization.DeserializationConfiguration
import org.jetbrains.kotlin.storage.StorageManager
@@ -42,4 +43,8 @@ class IncrementalPackagePartProvider(
// TODO
override fun findMetadataPackageParts(packageFqName: String): List<String> = TODO()
override fun getAnnotationsOnBinaryModule(moduleName: String): List<ClassId> {
return parent.getAnnotationsOnBinaryModule(moduleName)
}
}
@@ -0,0 +1,42 @@
/*
* 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.load.kotlin
import org.jetbrains.kotlin.descriptors.ClassOrPackageFragmentDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedMemberDescriptor
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf
fun getJvmModuleNameForDeserializedDescriptor(descriptor: DeclarationDescriptor): String? {
val parent = DescriptorUtils.getParentOfType(descriptor, ClassOrPackageFragmentDescriptor::class.java, false)
when {
parent is DeserializedClassDescriptor -> {
val classProto = parent.classProto
val nameResolver = parent.c.nameResolver
return classProto.getExtension(JvmProtoBuf.classModuleName)
?.takeIf { classProto.hasExtension(JvmProtoBuf.classModuleName) }
?.let(nameResolver::getString)
?: JvmAbi.DEFAULT_MODULE_NAME
}
descriptor is DeserializedMemberDescriptor -> {
val source = descriptor.containerSource
if (source is JvmPackagePartSource) {
val packageProto = source.packageProto
val nameResolver = source.nameResolver
return packageProto.getExtension(JvmProtoBuf.packageModuleName)
?.takeIf { packageProto.hasExtension(JvmProtoBuf.packageModuleName) }
?.let(nameResolver::getString)
?: JvmAbi.DEFAULT_MODULE_NAME
}
}
}
return null
}
@@ -30,6 +30,7 @@ import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.calls.tower.KotlinResolutionStatelessCallbacksImpl
import org.jetbrains.kotlin.resolve.checkers.ExperimentalUsageChecker
import org.jetbrains.kotlin.resolve.lazy.*
import org.jetbrains.kotlin.resolve.lazy.declarations.DeclarationProviderFactory
import org.jetbrains.kotlin.resolve.lazy.declarations.FileBasedDeclarationProviderFactory
@@ -63,6 +64,10 @@ fun StorageComponentContainer.configureModule(
private fun StorageComponentContainer.configurePlatformIndependentComponents() {
useImpl<SupertypeLoopCheckerImpl>()
useImpl<KotlinResolutionStatelessCallbacksImpl>()
useImpl<ExperimentalUsageChecker>()
useImpl<ExperimentalUsageChecker.Overrides>()
useImpl<ExperimentalUsageChecker.ClassifierUsage>()
}
fun StorageComponentContainer.configureModule(
@@ -0,0 +1,20 @@
/*
* 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
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.name.ClassId
interface ModuleAnnotationsResolver {
fun getAnnotationsOnContainingModule(descriptor: DeclarationDescriptor): List<ClassId>
companion object {
fun getInstance(project: Project): ModuleAnnotationsResolver =
ServiceManager.getService(project, ModuleAnnotationsResolver::class.java)
}
}
@@ -81,8 +81,7 @@ private val DEFAULT_DECLARATION_CHECKERS = listOf(
DelegationChecker(),
KClassWithIncorrectTypeArgumentChecker,
SuspendOperatorsCheckers,
InlineClassDeclarationChecker,
ExperimentalUsageChecker.Overrides
InlineClassDeclarationChecker
)
private val DEFAULT_CALL_CHECKERS = listOf(
@@ -92,12 +91,11 @@ private val DEFAULT_CALL_CHECKERS = listOf(
CoroutineSuspendCallChecker, BuilderFunctionsCallChecker, DslScopeViolationCallChecker, MissingDependencyClassChecker,
CallableReferenceCompatibilityChecker(), LateinitIntrinsicApplicabilityChecker,
UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker(),
PrimitiveNumericComparisonCallChecker, LambdaWithSuspendModifierCallChecker, ExperimentalUsageChecker
PrimitiveNumericComparisonCallChecker, LambdaWithSuspendModifierCallChecker
)
private val DEFAULT_TYPE_CHECKERS = emptyList<AdditionalTypeChecker>()
private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf(
DeprecatedClassifierUsageChecker(), ApiVersionClassifierUsageChecker, MissingDependencyClassChecker.ClassifierUsage,
ExperimentalUsageChecker.ClassifierUsage
DeprecatedClassifierUsageChecker(), ApiVersionClassifierUsageChecker, MissingDependencyClassChecker.ClassifierUsage
)
private val DEFAULT_ANNOTATION_CHECKERS = listOf<AdditionalAnnotationChecker>(
ExperimentalMarkerDeclarationAnnotationChecker
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.resolve.checkers
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.config.AnalysisFlag
import org.jetbrains.kotlin.config.LanguageVersionSettings
@@ -26,8 +27,7 @@ import org.jetbrains.kotlin.incremental.components.NoLookupLocation
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.DescriptorUtils
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
@@ -41,19 +41,8 @@ import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.utils.SmartSet
import org.jetbrains.kotlin.utils.addIfNotNull
object ExperimentalUsageChecker : CallChecker {
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")
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")
class ExperimentalUsageChecker(project: Project) : CallChecker {
private val moduleAnnotationsResolver = ModuleAnnotationsResolver.getInstance(project)
internal data class Experimentality(
val markerDescriptor: ClassDescriptor,
@@ -73,235 +62,267 @@ object ExperimentalUsageChecker : CallChecker {
}
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
checkExperimental(resolvedCall.resultingDescriptor, reportOn, context)
checkExperimental(resolvedCall.resultingDescriptor, reportOn, context, moduleAnnotationsResolver)
}
private fun checkExperimental(descriptor: DeclarationDescriptor, element: PsiElement, context: CheckerContext) {
val experimentalities = descriptor.loadExperimentalities()
if (experimentalities.isNotEmpty()) {
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
companion object {
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")
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 fun checkExperimental(
descriptor: DeclarationDescriptor,
element: PsiElement,
context: CheckerContext,
moduleAnnotationsResolver: ModuleAnnotationsResolver
) {
val experimentalities = descriptor.loadExperimentalities(moduleAnnotationsResolver)
if (experimentalities.isNotEmpty()) {
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
}
context.trace.report(diagnostic.on(element, experimentality.annotationFqName, isBodyUsageOfSourceOnlyExperimentality))
}
context.trace.report(diagnostic.on(element, experimentality.annotationFqName, isBodyUsageOfSourceOnlyExperimentality))
}
}
}
private fun checkExperimental(
experimentalities: Collection<Experimentality>,
element: PsiElement,
bindingContext: BindingContext,
languageVersionSettings: LanguageVersionSettings,
module: ModuleDescriptor,
report: (experimentality: Experimentality, isBodyUsageOfCompilationExperimentality: Boolean) -> Unit
) {
val isBodyUsageExceptPublicInline = element.isBodyUsage(bindingContext, allowPublicInline = false)
val isBodyUsage = isBodyUsageExceptPublicInline || element.isBodyUsage(bindingContext, allowPublicInline = true)
private fun checkExperimental(
experimentalities: Collection<Experimentality>,
element: PsiElement,
bindingContext: BindingContext,
languageVersionSettings: LanguageVersionSettings,
module: ModuleDescriptor,
report: (experimentality: Experimentality, isBodyUsageOfCompilationExperimentality: Boolean) -> Unit
) {
val isBodyUsageExceptPublicInline = element.isBodyUsage(bindingContext, allowPublicInline = false)
val isBodyUsage = isBodyUsageExceptPublicInline || element.isBodyUsage(bindingContext, allowPublicInline = true)
for (experimentality in experimentalities) {
val isBodyUsageOfCompilationExperimentality =
experimentality.isCompilationOnly && isBodyUsage
for (experimentality in experimentalities) {
val isBodyUsageOfCompilationExperimentality =
experimentality.isCompilationOnly && isBodyUsage
val isBodyUsageInSameModule =
experimentality.markerDescriptor.module == module && isBodyUsageExceptPublicInline
val isBodyUsageInSameModule =
experimentality.markerDescriptor.module == module && isBodyUsageExceptPublicInline
val annotationFqName = experimentality.annotationFqName
val isExperimentalityAccepted =
isBodyUsageInSameModule ||
(isBodyUsageOfCompilationExperimentality &&
element.hasContainerAnnotatedWithUseExperimental(annotationFqName, bindingContext, languageVersionSettings)) ||
element.propagates(annotationFqName, bindingContext, languageVersionSettings)
val annotationFqName = experimentality.annotationFqName
val isExperimentalityAccepted =
isBodyUsageInSameModule ||
(isBodyUsageOfCompilationExperimentality &&
element.hasContainerAnnotatedWithUseExperimental(annotationFqName, bindingContext, languageVersionSettings)) ||
element.propagates(annotationFqName, bindingContext, languageVersionSettings)
if (!isExperimentalityAccepted) {
report(experimentality, isBodyUsageOfCompilationExperimentality)
if (!isExperimentalityAccepted) {
report(experimentality, isBodyUsageOfCompilationExperimentality)
}
}
}
}
private fun DeclarationDescriptor.loadExperimentalities(): Set<Experimentality> {
val result = SmartSet.create<Experimentality>()
private fun DeclarationDescriptor.loadExperimentalities(
moduleAnnotationsResolver: ModuleAnnotationsResolver
): Set<Experimentality> {
val result = SmartSet.create<Experimentality>()
for (annotation in annotations) {
result.addIfNotNull(annotation.annotationClass?.loadExperimentalityForMarkerAnnotation())
}
val container = containingDeclaration
if (container is ClassDescriptor && this !is ConstructorDescriptor) {
for (annotation in container.annotations) {
for (annotation in annotations) {
result.addIfNotNull(annotation.annotationClass?.loadExperimentalityForMarkerAnnotation())
}
val container = containingDeclaration
if (container is ClassDescriptor && this !is ConstructorDescriptor) {
for (annotation in container.annotations) {
result.addIfNotNull(annotation.annotationClass?.loadExperimentalityForMarkerAnnotation())
}
}
for (moduleAnnotationClassId in moduleAnnotationsResolver.getAnnotationsOnContainingModule(this)) {
val annotationClass = module.findClassAcrossModuleDependencies(moduleAnnotationClassId)
result.addIfNotNull(annotationClass?.loadExperimentalityForMarkerAnnotation())
}
return result
}
return result
}
internal fun ClassDescriptor.loadExperimentalityForMarkerAnnotation(): Experimentality? {
val experimental = annotations.findAnnotation(EXPERIMENTAL_FQ_NAME) ?: 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
ERROR_LEVEL -> Experimentality.Severity.ERROR
else -> Experimentality.DEFAULT_SEVERITY
}
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(this, fqNameSafe, severity, impact)
}
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
// 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(bindingContext: BindingContext, allowPublicInline: Boolean): Boolean {
return anyParentMatches { element, parent ->
element == (parent as? KtDeclarationWithBody)?.bodyExpression?.takeIf {
allowPublicInline || !parent.isPublicInline(bindingContext)
} ||
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
}
}
private fun PsiElement.isPublicInline(bindingContext: BindingContext): Boolean {
val descriptor = when (this) {
is KtFunction -> bindingContext.get(BindingContext.FUNCTION, this)
is KtPropertyAccessor -> bindingContext.get(BindingContext.PROPERTY_ACCESSOR, this)
else -> null
}
} ?: Experimentality.DEFAULT_IMPACT
return Experimentality(this, 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.
// 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(bindingContext: BindingContext, allowPublicInline: Boolean): Boolean {
return anyParentMatches { element, parent ->
element == (parent as? KtDeclarationWithBody)?.bodyExpression?.takeIf {
allowPublicInline || !parent.isPublicInline(bindingContext)
} ||
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
return descriptor != null && descriptor.isInline && descriptor.effectiveVisibility().let {
it == EffectiveVisibility.Public ||
it == EffectiveVisibility.ProtectedBound ||
it is EffectiveVisibility.Protected
}
}
}
private fun PsiElement.isPublicInline(bindingContext: BindingContext): Boolean {
val descriptor = when (this) {
is KtFunction -> bindingContext.get(BindingContext.FUNCTION, this)
is KtPropertyAccessor -> bindingContext.get(BindingContext.PROPERTY_ACCESSOR, this)
else -> null
}
return descriptor != null && descriptor.isInline && descriptor.effectiveVisibility().let {
it == EffectiveVisibility.Public ||
it == EffectiveVisibility.ProtectedBound ||
it is EffectiveVisibility.Protected
}
}
// 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,
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,
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
// 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,
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
}
}
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
}
}
fun checkCompilerArguments(
module: ModuleDescriptor,
languageVersionSettings: LanguageVersionSettings,
reportError: (String) -> Unit,
reportWarning: (String) -> Unit
) {
// Ideally, we should run full resolution (with all classifier usage checkers) on classifiers used in "-Xexperimental" and
// "-Xuse-experimental" arguments. However, it's not easy to do this. This should be solved in the future with the support of
// module annotations. For now, we only check deprecations because this is needed to correctly retire unneeded compiler arguments.
val deprecationResolver = DeprecationResolver(LockBasedStorageManager(), languageVersionSettings)
fun checkAnnotation(fqName: String, allowNonCompilationImpact: Boolean): Boolean {
val descriptor = module.resolveClassByFqName(FqName(fqName), NoLookupLocation.FOR_NON_TRACKED_SCOPE)
val experimentality = descriptor?.loadExperimentalityForMarkerAnnotation()
val message = when {
descriptor == null ->
"Experimental API marker $fqName is unresolved. " +
"Please make sure it's present in the module dependencies"
experimentality == null ->
"Class $fqName is not an experimental API marker annotation"
!allowNonCompilationImpact && !experimentality.impact.all(Experimentality.Impact.COMPILATION::equals) ->
"Experimental API marker $fqName has impact other than COMPILATION, " +
"therefore it can't be used with -Xuse-experimental"
else -> {
for (deprecation in deprecationResolver.getDeprecations(descriptor)) {
val report = when (deprecation.deprecationLevel) {
DeprecationLevelValue.WARNING -> reportWarning
DeprecationLevelValue.ERROR, DeprecationLevelValue.HIDDEN -> reportError
// 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,
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
}
report("Experimental API marker $fqName is deprecated" + deprecation.message?.let { ". $it" }.orEmpty())
}
return 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
}
}
fun checkCompilerArguments(
module: ModuleDescriptor,
languageVersionSettings: LanguageVersionSettings,
reportError: (String) -> Unit,
reportWarning: (String) -> Unit
) {
// Ideally, we should run full resolution (with all classifier usage checkers) on classifiers used in "-Xexperimental" and
// "-Xuse-experimental" arguments. However, it's not easy to do this. This should be solved in the future with the support of
// module annotations. For now, we only check deprecations because this is needed to correctly retire unneeded compiler arguments.
val deprecationResolver = DeprecationResolver(LockBasedStorageManager(), languageVersionSettings)
fun checkAnnotation(fqName: String, allowNonCompilationImpact: Boolean): Boolean {
val descriptor = module.resolveClassByFqName(FqName(fqName), NoLookupLocation.FOR_NON_TRACKED_SCOPE)
val experimentality = descriptor?.loadExperimentalityForMarkerAnnotation()
val message = when {
descriptor == null ->
"Experimental API marker $fqName is unresolved. " +
"Please make sure it's present in the module dependencies"
experimentality == null ->
"Class $fqName is not an experimental API marker annotation"
!allowNonCompilationImpact && !experimentality.impact.all(Experimentality.Impact.COMPILATION::equals) ->
"Experimental API marker $fqName has impact other than COMPILATION, " +
"therefore it can't be used with -Xuse-experimental"
else -> {
for (deprecation in deprecationResolver.getDeprecations(descriptor)) {
val report = when (deprecation.deprecationLevel) {
DeprecationLevelValue.WARNING -> reportWarning
DeprecationLevelValue.ERROR, DeprecationLevelValue.HIDDEN -> reportError
}
report("Experimental API marker $fqName is deprecated" + deprecation.message?.let { ". $it" }.orEmpty())
}
return true
}
}
reportError(message)
return false
}
reportError(message)
val validExperimental =
languageVersionSettings.getFlag(AnalysisFlag.experimental)
.filter { checkAnnotation(it, allowNonCompilationImpact = true) }
val validUseExperimental =
languageVersionSettings.getFlag(AnalysisFlag.useExperimental)
.filter { checkAnnotation(it, allowNonCompilationImpact = false) }
return false
}
val validExperimental =
languageVersionSettings.getFlag(AnalysisFlag.experimental)
.filter { checkAnnotation(it, allowNonCompilationImpact = true) }
val validUseExperimental =
languageVersionSettings.getFlag(AnalysisFlag.useExperimental)
.filter { checkAnnotation(it, allowNonCompilationImpact = false) }
for (fqName in validExperimental.intersect(validUseExperimental)) {
reportError("'-Xuse-experimental=$fqName' has no effect because '-Xexperimental=$fqName' is used")
for (fqName in validExperimental.intersect(validUseExperimental)) {
reportError("'-Xuse-experimental=$fqName' has no effect because '-Xexperimental=$fqName' is used")
}
}
}
object ClassifierUsage : ClassifierUsageChecker {
class ClassifierUsage(project: Project) : ClassifierUsageChecker {
private val moduleAnnotationsResolver = ModuleAnnotationsResolver.getInstance(project)
override fun check(targetDescriptor: ClassifierDescriptor, element: PsiElement, context: ClassifierUsageCheckerContext) {
checkExperimental(targetDescriptor, element, context)
checkExperimental(targetDescriptor, element, context, moduleAnnotationsResolver)
}
}
object Overrides : DeclarationChecker {
class Overrides(project: Project) : DeclarationChecker {
private val moduleAnnotationsResolver = ModuleAnnotationsResolver.getInstance(project)
override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) {
if (descriptor !is CallableMemberDescriptor) return
val experimentalOverridden = descriptor.overriddenDescriptors.flatMap { member ->
member.loadExperimentalities().map { experimentality -> experimentality to member }
member.loadExperimentalities(moduleAnnotationsResolver).map { experimentality -> experimentality to member }
}.toMap()
val module = descriptor.module
@@ -0,0 +1,8 @@
package lib
@Experimental
annotation class ExperimentalAPI
class Foo
fun bar() {}
@@ -0,0 +1,7 @@
compiler/testData/experimental/jsExperimentalModule/usage.kt:5:15: error: this declaration is experimental and its usage must be marked with '@lib.ExperimentalAPI'
fun fail(foo: Foo) {
^
compiler/testData/experimental/jsExperimentalModule/usage.kt:6:5: error: this declaration is experimental and its usage must be marked with '@lib.ExperimentalAPI'
bar()
^
COMPILATION_ERROR
@@ -0,0 +1,12 @@
package usage
import lib.*
fun fail(foo: Foo) {
bar()
}
@ExperimentalAPI
fun ok(foo: Foo) {
bar()
}
@@ -0,0 +1,8 @@
package lib
@Experimental
annotation class ExperimentalAPI
class Foo
fun bar() {}
@@ -0,0 +1,7 @@
compiler/testData/experimental/jvmExperimentalModule/usage.kt:5:15: error: this declaration is experimental and its usage must be marked with '@lib.ExperimentalAPI'
fun fail(foo: Foo) {
^
compiler/testData/experimental/jvmExperimentalModule/usage.kt:6:5: error: this declaration is experimental and its usage must be marked with '@lib.ExperimentalAPI'
bar()
^
COMPILATION_ERROR
@@ -0,0 +1,12 @@
package usage
import lib.*
fun fail(foo: Foo) {
bar()
}
@ExperimentalAPI
fun ok(foo: Foo) {
bar()
}
@@ -99,10 +99,16 @@ abstract class AbstractKotlinCompilerIntegrationTest : TestCaseWithTmpdir() {
*
* @return the path to the corresponding .meta.js file, i.e. "[libraryName].meta.js"
*/
protected fun compileJsLibrary(libraryName: String): File {
protected fun compileJsLibrary(
libraryName: String,
additionalOptions: List<String> = emptyList(),
checkKotlinOutput: (String) -> Unit = { actual -> assertEquals(normalizeOutput("" to ExitCode.OK), actual) }
): File {
val destination = File(tmpdir, "$libraryName.js")
val output = compileKotlin(libraryName, destination, compiler = K2JSCompiler(), expectedFileName = null)
assertEquals(normalizeOutput("" to ExitCode.OK), normalizeOutput(output))
val output = compileKotlin(
libraryName, destination, compiler = K2JSCompiler(), additionalOptions = additionalOptions, expectedFileName = null
)
checkKotlinOutput(normalizeOutput(output))
return File(tmpdir, "$libraryName.meta.js")
}
@@ -0,0 +1,43 @@
/*
* 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.jvm.compiler
import org.jetbrains.kotlin.cli.js.K2JSCompiler
import java.io.File
class ExperimentalIntegrationTest : AbstractKotlinCompilerIntegrationTest() {
override val testDataPath: String
get() = "compiler/testData/experimental/"
fun testJvmExperimentalModule() {
val lib = compileLibrary(
"lib", additionalOptions = listOf(
"-Xskip-runtime-version-check",
"-language-version",
"1.3",
"-Xexperimental=lib.ExperimentalAPI"
),
checkKotlinOutput = { output -> output.trimEnd().endsWith("OK") }
)
compileKotlin("usage.kt", tmpdir, listOf(lib), additionalOptions = listOf("-Xskip-metadata-version-check"))
}
fun testJsExperimentalModule() {
val lib = compileJsLibrary(
"lib", additionalOptions = listOf(
"-Xskip-runtime-version-check",
"-language-version",
"1.3",
"-Xexperimental=lib.ExperimentalAPI"
),
checkKotlinOutput = { output -> output.trimEnd().endsWith("OK") }
)
compileKotlin(
"usage.kt", File(tmpdir, "usage.js"), listOf(lib), K2JSCompiler(),
additionalOptions = listOf("-Xskip-metadata-version-check")
)
}
}
@@ -18,6 +18,7 @@ package kotlin.reflect.jvm.internal.components
import org.jetbrains.kotlin.descriptors.PackagePartProvider
import org.jetbrains.kotlin.load.kotlin.ModuleMapping
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.serialization.deserialization.DeserializationConfiguration
import java.util.concurrent.ConcurrentHashMap
@@ -44,4 +45,9 @@ class RuntimePackagePartProvider(private val classLoader: ClassLoader) : Package
// TODO
override fun findMetadataPackageParts(packageFqName: String): List<String> = TODO()
override fun getAnnotationsOnBinaryModule(moduleName: String): List<ClassId> {
// TODO: load annotations from resource files
return emptyList()
}
}
@@ -16,6 +16,8 @@
package org.jetbrains.kotlin.descriptors
import org.jetbrains.kotlin.name.ClassId
interface PackagePartProvider {
/**
* @return JVM internal names of package parts existing in the package with the given FQ name.
@@ -31,9 +33,13 @@ interface PackagePartProvider {
*/
fun findMetadataPackageParts(packageFqName: String): List<String>
fun getAnnotationsOnBinaryModule(moduleName: String): List<ClassId>
object Empty : PackagePartProvider {
override fun findPackageParts(packageFqName: String): List<String> = emptyList()
override fun findMetadataPackageParts(packageFqName: String): List<String> = emptyList()
override fun getAnnotationsOnBinaryModule(moduleName: String): List<ClassId> = emptyList()
}
}
@@ -21,6 +21,7 @@ import com.intellij.util.indexing.FileBasedIndex
import org.jetbrains.kotlin.descriptors.PackagePartProvider
import org.jetbrains.kotlin.idea.vfilefinder.KotlinModuleMappingIndex
import org.jetbrains.kotlin.load.kotlin.PackageParts
import org.jetbrains.kotlin.name.ClassId
class IDEPackagePartProvider(val scope: GlobalSearchScope) : PackagePartProvider {
override fun findPackageParts(packageFqName: String): List<String> =
@@ -31,4 +32,8 @@ class IDEPackagePartProvider(val scope: GlobalSearchScope) : PackagePartProvider
private fun getPackageParts(packageFqName: String): MutableList<PackageParts> =
FileBasedIndex.getInstance().getValues(KotlinModuleMappingIndex.KEY, packageFqName, scope)
override fun getAnnotationsOnBinaryModule(moduleName: String): List<ClassId> {
throw UnsupportedOperationException()
}
}
@@ -0,0 +1,20 @@
/*
* 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.js.resolve
import org.jetbrains.kotlin.descriptors.ClassOrPackageFragmentDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedMemberDescriptor
import org.jetbrains.kotlin.serialization.js.KotlinJavascriptPackageFragment
fun getAnnotationsOnContainingJsModule(descriptor: DeclarationDescriptor): List<ClassId>? {
val parent = DescriptorUtils.getParentOfType(descriptor, ClassOrPackageFragmentDescriptor::class.java, false) ?: return emptyList()
val parentSource = (descriptor as? DeserializedMemberDescriptor)?.containerSource ?: parent.source
return (parentSource as? KotlinJavascriptPackageFragment.JsContainerSource)?.annotations
}