Report experimental diagnostics on inaccessible (wrt SinceKotlin) API

If a declaration is annotated both with SinceKotlin and WasExperimental
and the SinceKotlin version makes it inaccessible, the experimental
checker must regard it as experimental, with markers specified in the
WasExperimental annotation arguments. So only errors/warnings about the
experimentality are going to be reported in this case, and no error that
the declaration is unavailable because of a low API version
This commit is contained in:
Alexander Udalov
2018-05-04 16:23:40 +02:00
parent 1f0d3d9c7c
commit 7d74508529
6 changed files with 48 additions and 24 deletions
@@ -56,7 +56,8 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
} }
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
val experimentalities = resolvedCall.resultingDescriptor.loadExperimentalities(moduleAnnotationsResolver) val experimentalities =
resolvedCall.resultingDescriptor.loadExperimentalities(moduleAnnotationsResolver, context.languageVersionSettings)
reportNotAcceptedExperimentalities(experimentalities, reportOn, context) reportNotAcceptedExperimentalities(experimentalities, reportOn, context)
} }
@@ -89,7 +90,8 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
} }
private fun DeclarationDescriptor.loadExperimentalities( private fun DeclarationDescriptor.loadExperimentalities(
moduleAnnotationsResolver: ModuleAnnotationsResolver moduleAnnotationsResolver: ModuleAnnotationsResolver,
languageVersionSettings: LanguageVersionSettings
): Set<Experimentality> { ): Set<Experimentality> {
val result = SmartSet.create<Experimentality>() val result = SmartSet.create<Experimentality>()
@@ -97,9 +99,16 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
result.addIfNotNull(annotation.annotationClass?.loadExperimentalityForMarkerAnnotation()) result.addIfNotNull(annotation.annotationClass?.loadExperimentalityForMarkerAnnotation())
} }
if (annotations.any { it.fqName == WAS_EXPERIMENTAL_FQ_NAME }) {
val accessibility = checkSinceKotlinVersionAccessibility(languageVersionSettings)
if (accessibility is SinceKotlinAccessibility.NotAccessibleButWasExperimental) {
result.addAll(accessibility.markerClasses.map { it.loadExperimentalityForMarkerAnnotation() })
}
}
val container = containingDeclaration val container = containingDeclaration
if (container is ClassDescriptor && this !is ConstructorDescriptor) { if (container is ClassDescriptor && this !is ConstructorDescriptor) {
result.addAll(container.loadExperimentalities(moduleAnnotationsResolver)) result.addAll(container.loadExperimentalities(moduleAnnotationsResolver, languageVersionSettings))
} }
for (moduleAnnotationClassId in moduleAnnotationsResolver.getAnnotationsOnContainingModule(this)) { for (moduleAnnotationClassId in moduleAnnotationsResolver.getAnnotationsOnContainingModule(this)) {
@@ -169,13 +178,13 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
} }
} }
internal fun Annotated.loadWasExperimentalMarkerFqNames(): List<FqName> { internal fun Annotated.loadWasExperimentalMarkerClasses(): List<ClassDescriptor> {
val wasExperimental = annotations.findAnnotation(WAS_EXPERIMENTAL_FQ_NAME) val wasExperimental = annotations.findAnnotation(WAS_EXPERIMENTAL_FQ_NAME)
if (wasExperimental != null) { if (wasExperimental != null) {
val annotationClasses = wasExperimental.allValueArguments[WAS_EXPERIMENTAL_ANNOTATION_CLASS] val annotationClasses = wasExperimental.allValueArguments[WAS_EXPERIMENTAL_ANNOTATION_CLASS]
if (annotationClasses is ArrayValue) { if (annotationClasses is ArrayValue) {
return annotationClasses.value.mapNotNull { annotationClass -> return annotationClasses.value.mapNotNull { annotationClass ->
(annotationClass as? KClassValue)?.value?.constructor?.declarationDescriptor?.fqNameSafe (annotationClass as? KClassValue)?.value?.constructor?.declarationDescriptor as? ClassDescriptor
} }
} }
} }
@@ -243,7 +252,7 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
} }
} }
val experimentalities = targetDescriptor.loadExperimentalities(moduleAnnotationsResolver) val experimentalities = targetDescriptor.loadExperimentalities(moduleAnnotationsResolver, context.languageVersionSettings)
reportNotAcceptedExperimentalities(experimentalities, element, context) reportNotAcceptedExperimentalities(experimentalities, element, context)
val targetClass = when (targetDescriptor) { val targetClass = when (targetDescriptor) {
@@ -311,7 +320,8 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
if (descriptor !is CallableMemberDescriptor) return if (descriptor !is CallableMemberDescriptor) return
val experimentalOverridden = descriptor.overriddenDescriptors.flatMap { member -> val experimentalOverridden = descriptor.overriddenDescriptors.flatMap { member ->
member.loadExperimentalities(moduleAnnotationsResolver).map { experimentality -> experimentality to member } member.loadExperimentalities(moduleAnnotationsResolver, context.languageVersionSettings)
.map { experimentality -> experimentality to member }
}.toMap() }.toMap()
for ((experimentality, member) in experimentalOverridden) { for ((experimentality, member) in experimentalOverridden) {
@@ -227,8 +227,8 @@ class DeprecationResolver(
if (sinceKotlinAccessibility is SinceKotlinAccessibility.NotAccessibleButWasExperimental) { if (sinceKotlinAccessibility is SinceKotlinAccessibility.NotAccessibleButWasExperimental) {
if (call != null && bindingContext != null) { if (call != null && bindingContext != null) {
return with(ExperimentalUsageChecker) { return with(ExperimentalUsageChecker) {
sinceKotlinAccessibility.annotationFqNames.any { fqName -> sinceKotlinAccessibility.markerClasses.any { classDescriptor ->
!call.callElement.isExperimentalityAccepted(fqName, languageVersionSettings, bindingContext) !call.callElement.isExperimentalityAccepted(classDescriptor.fqNameSafe, languageVersionSettings, bindingContext)
} }
} }
} }
@@ -9,7 +9,6 @@ import org.jetbrains.kotlin.config.ApiVersion
import org.jetbrains.kotlin.config.LanguageVersionSettings import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.impl.TypeAliasConstructorDescriptor import org.jetbrains.kotlin.descriptors.impl.TypeAliasConstructorDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.calls.util.FakeCallableDescriptorForTypeAliasObject import org.jetbrains.kotlin.resolve.calls.util.FakeCallableDescriptorForTypeAliasObject
import org.jetbrains.kotlin.resolve.checkers.ExperimentalUsageChecker import org.jetbrains.kotlin.resolve.checkers.ExperimentalUsageChecker
@@ -18,7 +17,7 @@ sealed class SinceKotlinAccessibility {
data class NotAccessibleButWasExperimental( data class NotAccessibleButWasExperimental(
val version: ApiVersion, val version: ApiVersion,
val annotationFqNames: List<FqName> val markerClasses: List<ClassDescriptor>
) : SinceKotlinAccessibility() ) : SinceKotlinAccessibility()
data class NotAccessible( data class NotAccessible(
@@ -38,7 +37,7 @@ fun DeclarationDescriptor.checkSinceKotlinVersionAccessibility(languageVersionSe
// 3) The value as a version is not greater than our API version // 3) The value as a version is not greater than our API version
if (version == null || version <= languageVersionSettings.apiVersion) return SinceKotlinAccessibility.Accessible if (version == null || version <= languageVersionSettings.apiVersion) return SinceKotlinAccessibility.Accessible
val wasExperimentalFqNames = value.wasExperimentalMarkerFqNames val wasExperimentalFqNames = value.wasExperimentalMarkerClasses
if (wasExperimentalFqNames.isNotEmpty()) { if (wasExperimentalFqNames.isNotEmpty()) {
return SinceKotlinAccessibility.NotAccessibleButWasExperimental(version, wasExperimentalFqNames) return SinceKotlinAccessibility.NotAccessibleButWasExperimental(version, wasExperimentalFqNames)
} }
@@ -48,7 +47,7 @@ fun DeclarationDescriptor.checkSinceKotlinVersionAccessibility(languageVersionSe
private data class SinceKotlinValue( private data class SinceKotlinValue(
val apiVersion: ApiVersion, val apiVersion: ApiVersion,
val wasExperimentalMarkerFqNames: List<FqName> val wasExperimentalMarkerClasses: List<ClassDescriptor>
) )
/** /**
@@ -56,7 +55,7 @@ private data class SinceKotlinValue(
* or the minimal value of the version from all declarations annotated with [SinceKotlin] otherwise. * or the minimal value of the version from all declarations annotated with [SinceKotlin] otherwise.
*/ */
private fun getSinceKotlinVersionByOverridden(descriptor: CallableMemberDescriptor): SinceKotlinValue? { private fun getSinceKotlinVersionByOverridden(descriptor: CallableMemberDescriptor): SinceKotlinValue? {
// TODO: combine wasExperimentalMarkerFqNames in case of several members with the same minimal API version // TODO: combine wasExperimentalMarkerClasses in case of several members with the same minimal API version
return DescriptorUtils.getAllOverriddenDeclarations(descriptor).map { it.getOwnSinceKotlinVersion() ?: return null } return DescriptorUtils.getAllOverriddenDeclarations(descriptor).map { it.getOwnSinceKotlinVersion() ?: return null }
.minBy { it.apiVersion } .minBy { it.apiVersion }
} }
@@ -73,11 +72,11 @@ private fun DeclarationDescriptor.getOwnSinceKotlinVersion(): SinceKotlinValue?
val apiVersion = (annotations.findAnnotation(SINCE_KOTLIN_FQ_NAME)?.allValueArguments?.values?.singleOrNull()?.value as? String) val apiVersion = (annotations.findAnnotation(SINCE_KOTLIN_FQ_NAME)?.allValueArguments?.values?.singleOrNull()?.value as? String)
?.let(ApiVersion.Companion::parse) ?.let(ApiVersion.Companion::parse)
if (apiVersion != null) { if (apiVersion != null) {
// TODO: combine wasExperimentalMarkerFqNames in case of several associated declarations with the same maximal API version // TODO: combine wasExperimentalMarkerClasses in case of several associated declarations with the same maximal API version
if (result == null || apiVersion > result!!.apiVersion) { if (result == null || apiVersion > result!!.apiVersion) {
result = SinceKotlinValue( result = SinceKotlinValue(
apiVersion, apiVersion,
with(ExperimentalUsageChecker) { loadWasExperimentalMarkerFqNames() } with(ExperimentalUsageChecker) { loadWasExperimentalMarkerClasses() }
) )
} }
} }
@@ -1,6 +1,6 @@
// !API_VERSION: 1.2 // !API_VERSION: 1.2
// !USE_EXPERIMENTAL: kotlin.Experimental // !USE_EXPERIMENTAL: kotlin.Experimental
// !DIAGNOSTICS: -INVISIBLE_MEMBER -INVISIBLE_REFERENCE -NEWER_VERSION_IN_SINCE_KOTLIN // !DIAGNOSTICS: -INVISIBLE_MEMBER -INVISIBLE_REFERENCE -NEWER_VERSION_IN_SINCE_KOTLIN -UNUSED_PARAMETER
@SinceKotlin("1.3") @SinceKotlin("1.3")
fun newPublishedFun() {} fun newPublishedFun() {}
@@ -21,7 +21,15 @@ val newValExperimentalInThePast = ""
@WasExperimental(Marker::class) @WasExperimental(Marker::class)
class NewClassExperimentalInThePast class NewClassExperimentalInThePast
fun use1() { @SinceKotlin("1.3")
@WasExperimental(Marker::class)
typealias TypeAliasToNewClass = <!EXPERIMENTAL_API_USAGE_ERROR!>NewClassExperimentalInThePast<!>
fun use1(
c1: <!EXPERIMENTAL_API_USAGE_ERROR!>NewClassExperimentalInThePast<!>,
t1: <!EXPERIMENTAL_API_USAGE_ERROR!>TypeAliasToNewClass<!>
) {
<!UNRESOLVED_REFERENCE!>newPublishedFun<!>() <!UNRESOLVED_REFERENCE!>newPublishedFun<!>()
<!UNRESOLVED_REFERENCE!>newFunExperimentalInThePast<!>() <!UNRESOLVED_REFERENCE!>newFunExperimentalInThePast<!>()
<!UNRESOLVED_REFERENCE!>newValExperimentalInThePast<!> <!UNRESOLVED_REFERENCE!>newValExperimentalInThePast<!>
@@ -29,7 +37,10 @@ fun use1() {
} }
@UseExperimental(Marker::class) @UseExperimental(Marker::class)
fun use2() { fun use2(
c2: NewClassExperimentalInThePast,
t2: TypeAliasToNewClass
) {
<!UNRESOLVED_REFERENCE!>newPublishedFun<!>() <!UNRESOLVED_REFERENCE!>newPublishedFun<!>()
newFunExperimentalInThePast() newFunExperimentalInThePast()
newValExperimentalInThePast newValExperimentalInThePast
@@ -37,7 +48,10 @@ fun use2() {
} }
@Marker @Marker
fun use3() { fun use3(
c3: NewClassExperimentalInThePast,
t3: TypeAliasToNewClass
) {
<!UNRESOLVED_REFERENCE!>newPublishedFun<!>() <!UNRESOLVED_REFERENCE!>newPublishedFun<!>()
newFunExperimentalInThePast() newFunExperimentalInThePast()
newValExperimentalInThePast newValExperimentalInThePast
@@ -3,9 +3,9 @@ package
@kotlin.SinceKotlin(version = "1.3") @kotlin.WasExperimental(markerClass = {Marker::class}) public val newValExperimentalInThePast: kotlin.String = "" @kotlin.SinceKotlin(version = "1.3") @kotlin.WasExperimental(markerClass = {Marker::class}) public val newValExperimentalInThePast: kotlin.String = ""
@kotlin.SinceKotlin(version = "1.3") @kotlin.WasExperimental(markerClass = {Marker::class}) public fun newFunExperimentalInThePast(): kotlin.Unit @kotlin.SinceKotlin(version = "1.3") @kotlin.WasExperimental(markerClass = {Marker::class}) public fun newFunExperimentalInThePast(): kotlin.Unit
@kotlin.SinceKotlin(version = "1.3") public fun newPublishedFun(): kotlin.Unit @kotlin.SinceKotlin(version = "1.3") public fun newPublishedFun(): kotlin.Unit
public fun use1(): kotlin.Unit public fun use1(/*0*/ c1: NewClassExperimentalInThePast, /*1*/ t1: TypeAliasToNewClass /* = NewClassExperimentalInThePast */): kotlin.Unit
@kotlin.UseExperimental(markerClass = {Marker::class}) public fun use2(): kotlin.Unit @kotlin.UseExperimental(markerClass = {Marker::class}) public fun use2(/*0*/ c2: NewClassExperimentalInThePast, /*1*/ t2: TypeAliasToNewClass /* = NewClassExperimentalInThePast */): kotlin.Unit
@Marker public fun use3(): kotlin.Unit @Marker public fun use3(/*0*/ c3: NewClassExperimentalInThePast, /*1*/ t3: TypeAliasToNewClass /* = NewClassExperimentalInThePast */): kotlin.Unit
@kotlin.Experimental public final annotation class Marker : kotlin.Annotation { @kotlin.Experimental public final annotation class Marker : kotlin.Annotation {
public constructor Marker() public constructor Marker()
@@ -20,3 +20,4 @@ public fun use1(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
} }
@kotlin.SinceKotlin(version = "1.3") @kotlin.WasExperimental(markerClass = {Marker::class}) public typealias TypeAliasToNewClass = NewClassExperimentalInThePast
@@ -52,7 +52,7 @@ annotation class UseExperimental(
) )
@Target(CLASS, PROPERTY, CONSTRUCTOR, FUNCTION) @Target(CLASS, PROPERTY, CONSTRUCTOR, FUNCTION, TYPEALIAS)
@Retention(BINARY) @Retention(BINARY)
internal annotation class WasExperimental( internal annotation class WasExperimental(
vararg val markerClass: KClass<out Annotation> vararg val markerClass: KClass<out Annotation>