Support custom message in RequiresOptIn

#KT-34648 Fixed
This commit is contained in:
Alexander Udalov
2019-12-18 19:22:38 +01:00
parent 9187a85aaf
commit f954a6c812
41 changed files with 124 additions and 82 deletions
@@ -255,11 +255,11 @@ 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);
DiagnosticFactory1<PsiElement, FqName> EXPERIMENTAL_API_USAGE = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<PsiElement, FqName> EXPERIMENTAL_API_USAGE_ERROR = DiagnosticFactory1.create(ERROR);
DiagnosticFactory1<PsiElement, String> EXPERIMENTAL_API_USAGE = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<PsiElement, String> EXPERIMENTAL_API_USAGE_ERROR = DiagnosticFactory1.create(ERROR);
DiagnosticFactory2<PsiElement, FqName, DeclarationDescriptor> EXPERIMENTAL_OVERRIDE = DiagnosticFactory2.create(WARNING);
DiagnosticFactory2<PsiElement, FqName, DeclarationDescriptor> EXPERIMENTAL_OVERRIDE_ERROR = DiagnosticFactory2.create(ERROR);
DiagnosticFactory1<PsiElement, String> EXPERIMENTAL_OVERRIDE = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<PsiElement, String> EXPERIMENTAL_OVERRIDE_ERROR = DiagnosticFactory1.create(ERROR);
DiagnosticFactory0<PsiElement> EXPERIMENTAL_IS_NOT_ENABLED = DiagnosticFactory0.create(WARNING);
DiagnosticFactory0<PsiElement> EXPERIMENTAL_CAN_ONLY_BE_USED_AS_ANNOTATION = DiagnosticFactory0.create(ERROR);
@@ -270,8 +270,8 @@ public interface Errors {
DiagnosticFactory1<KtAnnotationEntry, FqName> USE_EXPERIMENTAL_ARGUMENT_IS_NOT_MARKER = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<KtAnnotationEntry, String> EXPERIMENTAL_ANNOTATION_WITH_WRONG_TARGET = DiagnosticFactory1.create(ERROR);
DiagnosticFactory1<PsiElement, FqName> EXPERIMENTAL_UNSIGNED_LITERALS = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<PsiElement, FqName> EXPERIMENTAL_UNSIGNED_LITERALS_ERROR = DiagnosticFactory1.create(ERROR);
DiagnosticFactory1<PsiElement, String> EXPERIMENTAL_UNSIGNED_LITERALS = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<PsiElement, String> EXPERIMENTAL_UNSIGNED_LITERALS_ERROR = DiagnosticFactory1.create(ERROR);
DiagnosticFactory0<PsiElement> NON_PARENTHESIZED_ANNOTATIONS_ON_FUNCTIONAL_TYPES = DiagnosticFactory0.create(ERROR);
@@ -147,11 +147,11 @@ 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);
MAP.put(EXPERIMENTAL_API_USAGE, "This declaration is experimental and its usage should be marked with ''@{0}'' or ''@OptIn({0}::class)''", TO_STRING);
MAP.put(EXPERIMENTAL_API_USAGE_ERROR, "This declaration is experimental and its usage must be marked with ''@{0}'' or ''@OptIn({0}::class)''", TO_STRING);
MAP.put(EXPERIMENTAL_API_USAGE, "{0}", STRING);
MAP.put(EXPERIMENTAL_API_USAGE_ERROR, "{0}", STRING);
MAP.put(EXPERIMENTAL_OVERRIDE, "This declaration overrides experimental member of supertype ''{1}'' and should be annotated with ''@{0}''", TO_STRING, NAME);
MAP.put(EXPERIMENTAL_OVERRIDE_ERROR, "This declaration overrides experimental member of supertype ''{1}'' and must be annotated with ''@{0}''", TO_STRING, NAME);
MAP.put(EXPERIMENTAL_OVERRIDE, "{0}", STRING);
MAP.put(EXPERIMENTAL_OVERRIDE_ERROR, "{0}", STRING);
MAP.put(EXPERIMENTAL_IS_NOT_ENABLED, "This class can only be used with the compiler argument '-Xopt-in=kotlin.RequiresOptIn'");
MAP.put(EXPERIMENTAL_CAN_ONLY_BE_USED_AS_ANNOTATION, "This class can only be used as an annotation");
@@ -161,8 +161,8 @@ public class DefaultErrorMessages {
MAP.put(USE_EXPERIMENTAL_ARGUMENT_IS_NOT_MARKER, "Annotation ''{0}'' is not an opt-in requirement marker, therefore its usage in @OptIn is ignored", TO_STRING);
MAP.put(EXPERIMENTAL_ANNOTATION_WITH_WRONG_TARGET, "Opt-in requirement marker annotation cannot be used on the following code elements: {0}. Please remove these targets", STRING);
MAP.put(EXPERIMENTAL_UNSIGNED_LITERALS, "Unsigned literals are experimental and their usages should be marked with ''@{0}'' or ''@OptIn({0}::class)''", TO_STRING);
MAP.put(EXPERIMENTAL_UNSIGNED_LITERALS_ERROR, "Unsigned literals are experimental and their usages must be marked with ''@{0}'' or ''@OptIn({0}::class)''", TO_STRING);
MAP.put(EXPERIMENTAL_UNSIGNED_LITERALS, "{0}", STRING);
MAP.put(EXPERIMENTAL_UNSIGNED_LITERALS_ERROR, "{0}", STRING);
MAP.put(NON_PARENTHESIZED_ANNOTATIONS_ON_FUNCTIONAL_TYPES, "Non-parenthesized annotations on function types without receiver aren't yet supported (see KT-31734 for details)");
@@ -37,6 +37,7 @@ 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.constants.StringValue
import org.jetbrains.kotlin.resolve.deprecation.CoroutineCompatibilitySupport
import org.jetbrains.kotlin.resolve.deprecation.DeprecationLevelValue
import org.jetbrains.kotlin.resolve.deprecation.DeprecationResolver
@@ -51,7 +52,7 @@ import org.jetbrains.kotlin.utils.addIfNotNull
class ExperimentalUsageChecker(project: Project) : CallChecker {
private val moduleAnnotationsResolver = ModuleAnnotationsResolver.getInstance(project)
data class Experimentality(val annotationFqName: FqName, val severity: Severity) {
data class Experimentality(val annotationFqName: FqName, val severity: Severity, val message: String?) {
enum class Severity { WARNING, ERROR }
companion object {
@@ -59,9 +60,14 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
}
}
class ExperimentalityDiagnostic(
val factory: DiagnosticFactory1<PsiElement, String>,
val defaultMessage: (FqName) -> String
)
data class ExperimentalityDiagnostics(
val warning: DiagnosticFactory1<PsiElement, FqName>,
val error: DiagnosticFactory1<PsiElement, FqName>
val warning: ExperimentalityDiagnostic,
val error: ExperimentalityDiagnostic
)
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
@@ -84,18 +90,30 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
internal val WAS_EXPERIMENTAL_ANNOTATION_CLASS = Name.identifier("markerClass")
private val LEVEL = Name.identifier("level")
private val MESSAGE = Name.identifier("message")
private val WARNING_LEVEL = Name.identifier("WARNING")
private val ERROR_LEVEL = Name.identifier("ERROR")
private val EXPERIMENTAL_API_DIAGNOSTICS = ExperimentalityDiagnostics(
Errors.EXPERIMENTAL_API_USAGE, Errors.EXPERIMENTAL_API_USAGE_ERROR
internal fun getDefaultDiagnosticMessage(prefix: String) = fun(fqName: FqName): String {
return "$prefix with '@${fqName.asString()}' or '@OptIn(${fqName.asString()}::class)'"
}
private val USAGE_DIAGNOSTICS = ExperimentalityDiagnostics(
warning = ExperimentalityDiagnostic(
Errors.EXPERIMENTAL_API_USAGE,
getDefaultDiagnosticMessage("This declaration is experimental and its usage should be marked")
),
error = ExperimentalityDiagnostic(
Errors.EXPERIMENTAL_API_USAGE_ERROR,
getDefaultDiagnosticMessage("This declaration is experimental and its usage must be marked")
)
)
fun reportNotAcceptedExperimentalities(
experimentalities: Collection<Experimentality>, element: PsiElement, context: CheckerContext
) {
reportNotAcceptedExperimentalities(
experimentalities, element, context.languageVersionSettings, context.trace, EXPERIMENTAL_API_DIAGNOSTICS
experimentalities, element, context.languageVersionSettings, context.trace, USAGE_DIAGNOSTICS
)
}
@@ -106,13 +124,13 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
trace: BindingTrace,
diagnostics: ExperimentalityDiagnostics
) {
for ((annotationFqName, severity) in experimentalities) {
for ((annotationFqName, severity, message) in experimentalities) {
if (!element.isExperimentalityAccepted(annotationFqName, languageVersionSettings, trace.bindingContext)) {
val diagnostic = when (severity) {
Experimentality.Severity.WARNING -> diagnostics.warning
Experimentality.Severity.ERROR -> diagnostics.error
}
trace.reportDiagnosticOnce(diagnostic.on(element, annotationFqName))
trace.reportDiagnosticOnce(diagnostic.factory.on(element, message ?: diagnostic.defaultMessage(annotationFqName)))
}
}
}
@@ -153,13 +171,16 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
?: annotations.findAnnotation(OLD_EXPERIMENTAL_FQ_NAME)
?: return null
val severity = when ((experimental.allValueArguments[LEVEL] as? EnumValue)?.enumEntryName) {
val arguments = experimental.allValueArguments
val severity = when ((arguments[LEVEL] as? EnumValue)?.enumEntryName) {
WARNING_LEVEL -> Experimentality.Severity.WARNING
ERROR_LEVEL -> Experimentality.Severity.ERROR
else -> Experimentality.DEFAULT_SEVERITY
}
return Experimentality(fqNameSafe, severity)
val message = (arguments[MESSAGE] as? StringValue)?.value
return Experimentality(fqNameSafe, severity, message)
}
private fun PsiElement.isExperimentalityAccepted(annotationFqName: FqName, context: CheckerContext): Boolean =
@@ -344,12 +365,17 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
for ((experimentality, member) in experimentalOverridden) {
if (!declaration.isExperimentalityAccepted(experimentality.annotationFqName, context)) {
val diagnostic = when (experimentality.severity) {
Experimentality.Severity.WARNING -> Errors.EXPERIMENTAL_OVERRIDE
Experimentality.Severity.ERROR -> Errors.EXPERIMENTAL_OVERRIDE_ERROR
}
val reportOn = (declaration as? KtNamedDeclaration)?.nameIdentifier ?: declaration
context.trace.report(diagnostic.on(reportOn, experimentality.annotationFqName, member.containingDeclaration))
val (diagnostic, defaultMessageVerb) = when (experimentality.severity) {
Experimentality.Severity.WARNING -> Errors.EXPERIMENTAL_OVERRIDE to "should"
Experimentality.Severity.ERROR -> Errors.EXPERIMENTAL_OVERRIDE_ERROR to "must"
}
val message = experimentality.message
?: "This declaration overrides experimental member of supertype " +
"'${member.containingDeclaration.name.asString()}' and $defaultMessageVerb be annotated " +
"with '@${experimentality.annotationFqName.asString()}'"
context.trace.report(diagnostic.on(reportOn, message))
}
}
}
@@ -319,7 +319,14 @@ class ConstantExpressionEvaluator(
companion object {
private val EXPERIMENTAL_UNSIGNED_LITERALS_DIAGNOSTICS = ExperimentalUsageChecker.ExperimentalityDiagnostics(
Errors.EXPERIMENTAL_UNSIGNED_LITERALS, Errors.EXPERIMENTAL_UNSIGNED_LITERALS_ERROR
warning = ExperimentalUsageChecker.ExperimentalityDiagnostic(
Errors.EXPERIMENTAL_UNSIGNED_LITERALS,
ExperimentalUsageChecker.getDefaultDiagnosticMessage("Unsigned literals are experimental and their usages should be marked")
),
error = ExperimentalUsageChecker.ExperimentalityDiagnostic(
Errors.EXPERIMENTAL_UNSIGNED_LITERALS_ERROR,
ExperimentalUsageChecker.getDefaultDiagnosticMessage("Unsigned literals are experimental and their usages must be marked")
)
)
@JvmStatic