From 9129250a495b5dcfd6f2641623c8fcd4e07cba51 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Fri, 12 May 2023 17:50:42 +0200 Subject: [PATCH] [inspection] Extract logic of checking @WasExperimental from the opt-in inspection It will be used in subsequent commit. ^KTIJ-25447 GitOrigin-RevId: 49a8c978d870e3d772cf374484ced898cd4587c6 --- .../UnnecessaryOptInAnnotationInspection.kt | 34 +-------- ...sExperimentalOptInsNecessityCheckerFe10.kt | 74 +++++++++++++++++++ 2 files changed, 78 insertions(+), 30 deletions(-) create mode 100644 plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/util/WasExperimentalOptInsNecessityCheckerFe10.kt diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/UnnecessaryOptInAnnotationInspection.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/UnnecessaryOptInAnnotationInspection.kt index 5ffe3408aa0a..875b23287c4b 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/UnnecessaryOptInAnnotationInspection.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/UnnecessaryOptInAnnotationInspection.kt @@ -57,6 +57,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection import org.jetbrains.kotlin.idea.inspections.CanSealedSubClassBeObjectInspection.Companion.asKtClass import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor +import org.jetbrains.kotlin.idea.util.WasExperimentalOptInsNecessityCheckerFe10 import org.jetbrains.kotlin.resolve.descriptorUtil.* class UnnecessaryOptInAnnotationInspection : AbstractKotlinInspection() { @@ -300,36 +301,9 @@ private class MarkerCollector(private val resolutionFacade: ResolutionFacade) { foundMarkers += annotationFqName } - val wasExperimental = annotations.findAnnotation(OptInNames.WAS_EXPERIMENTAL_FQ_NAME) ?: continue - val sinceKotlin = annotations.findAnnotation(SINCE_KOTLIN_FQ_NAME) ?: continue - - // If there are both `@SinceKotlin` and `@WasExperimental` annotations, - // and Kotlin API version of the module is less than the version specified by `@SinceKotlin`, - // then the `@OptIn` for `@WasExperimental` marker is necessary and should be added - // to the set of found markers. - // - // For example, consider a function - // ``` - // @SinceKotlin("1.6") - // @WasExperimental(Marker::class) - // fun foo() { ... } - // ``` - // This combination of annotations means that `foo` was experimental before Kotlin 1.6 - // and required `@OptIn(Marker::class) or `@Marker` annotation. When the client code - // is compiled as Kotlin 1.6 code, there are no problems, and the `@OptIn(Marker::class)` - // annotation would not be necessary. At the same time, when the code is compiled with - // `apiVersion = 1.5`, the non-experimental declaration of `foo` will be hidden - // from the resolver, so `@OptIn` is necessary for the code to compile. - val sinceKotlinApiVersion = sinceKotlin.allValueArguments[VERSION_ARGUMENT] - ?.safeAs()?.value?.let { - ApiVersion.parse(it) - } - - if (sinceKotlinApiVersion != null && moduleApiVersion < sinceKotlinApiVersion) { - wasExperimental.allValueArguments[OptInNames.WAS_EXPERIMENTAL_ANNOTATION_CLASS]?.safeAs()?.value - ?.mapNotNull { it.safeAs()?.getArgumentType(module)?.fqName } - ?.forEach { foundMarkers.add(it) } - } + WasExperimentalOptInsNecessityCheckerFe10 + .getNecessaryOptInsFromWasExperimental(annotations, module, moduleApiVersion) + .forEach { foundMarkers.add(it) } } } diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/util/WasExperimentalOptInsNecessityCheckerFe10.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/util/WasExperimentalOptInsNecessityCheckerFe10.kt new file mode 100644 index 000000000000..84a6c5147f35 --- /dev/null +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/util/WasExperimentalOptInsNecessityCheckerFe10.kt @@ -0,0 +1,74 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.util + +import org.jetbrains.kotlin.config.ApiVersion +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.idea.refactoring.fqName.fqName +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.SINCE_KOTLIN_FQ_NAME +import org.jetbrains.kotlin.resolve.checkers.OptInNames +import org.jetbrains.kotlin.resolve.constants.ArrayValue +import org.jetbrains.kotlin.resolve.constants.KClassValue +import org.jetbrains.kotlin.resolve.constants.StringValue +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + + +/** + * See [org.jetbrains.kotlin.idea.codeinsights.impl.base.inspection.WasExperimentalOptInsNecessityChecker] + */ +internal object WasExperimentalOptInsNecessityCheckerFe10 { + private val VERSION_ARGUMENT = Name.identifier("version") + + + // If there are both `@SinceKotlin` and `@WasExperimental` annotations, + // and Kotlin API version of the module is less than the version specified by `@SinceKotlin`, + // then the `@OptIn` for `@WasExperimental` marker is necessary. + // + // For example, consider a function + // ``` + // @SinceKotlin("1.6") + // @WasExperimental(Marker::class) + // fun foo() { ... } + // ``` + // This combination of annotations means that `foo` was experimental before Kotlin 1.6 + // and required `@OptIn(Marker::class) or `@Marker` annotation. When the client code + // is compiled as Kotlin 1.6 code, there are no problems, and the `@OptIn(Marker::class)` + // annotation would not be necessary. At the same time, when the code is compiled with + // `apiVersion = 1.5`, the non-experimental declaration of `foo` will be hidden + // from the resolver, so `@OptIn` is necessary for the code to compile. + fun getNecessaryOptInsFromWasExperimental( + annotations: Annotations, + module: ModuleDescriptor, + moduleApiVersion: ApiVersion, + ): Collection { + val wasExperimental = annotations.findAnnotation(OptInNames.WAS_EXPERIMENTAL_FQ_NAME) + val sinceApiVersion = getSinceKotlinAnnotationApiVersionArgumentIfPresent(annotations) + + if (wasExperimental == null || sinceApiVersion == null || moduleApiVersion >= sinceApiVersion) { + return emptyList() + } + return getWasExperimentalAnnotationMarkerClassArgument(wasExperimental, module) + } + + private fun getSinceKotlinAnnotationApiVersionArgumentIfPresent(annotations: Annotations): ApiVersion? { + val sinceKotlin = annotations.findAnnotation(SINCE_KOTLIN_FQ_NAME) ?: return null + return sinceKotlin.allValueArguments[VERSION_ARGUMENT] + ?.safeAs() + ?.value + ?.let { ApiVersion.parse(it) } + } + + private fun getWasExperimentalAnnotationMarkerClassArgument( + annotation: AnnotationDescriptor, + module: ModuleDescriptor, + ): Collection { + return annotation.allValueArguments[OptInNames.WAS_EXPERIMENTAL_ANNOTATION_CLASS] + ?.safeAs() + ?.value + ?.mapNotNull { (it as? KClassValue)?.getArgumentType(module)?.fqName } + ?: emptyList() + } +}