Make FE1.0/FIR OptIn override message more clear

This commit is contained in:
Mikhail Glukhikh
2022-03-03 10:49:56 +03:00
committed by Space
parent d823020c07
commit 779aa75855
11 changed files with 133 additions and 16 deletions
@@ -37,17 +37,41 @@ import org.jetbrains.kotlin.utils.SmartSet
import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.addIfNotNull
object FirOptInUsageBaseChecker { object FirOptInUsageBaseChecker {
data class Experimentality(val annotationClassId: ClassId, val severity: Severity, val message: String?) { data class Experimentality(
val annotationClassId: ClassId,
val severity: Severity,
val message: String?,
val supertypeName: String? = null
) {
enum class Severity { WARNING, ERROR } enum class Severity { WARNING, ERROR }
companion object { companion object {
val DEFAULT_SEVERITY = Severity.ERROR val DEFAULT_SEVERITY = Severity.ERROR
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Experimentality) return false
if (annotationClassId != other.annotationClassId) return false
if (severity != other.severity) return false
if (message != other.message) return false
return true
}
override fun hashCode(): Int {
var result = annotationClassId.hashCode()
result = 31 * result + severity.hashCode()
result = 31 * result + (message?.hashCode() ?: 0)
return result
}
} }
fun FirRegularClassSymbol.loadExperimentalityForMarkerAnnotation(): Experimentality? { // Note: receiver is an OptIn marker class and parameter is an annotated member owner class / self class name
fun FirRegularClassSymbol.loadExperimentalityForMarkerAnnotation(annotatedOwnerClassName: String? = null): Experimentality? {
ensureResolved(FirResolvePhase.BODY_RESOLVE) ensureResolved(FirResolvePhase.BODY_RESOLVE)
@OptIn(SymbolInternals::class) @OptIn(SymbolInternals::class)
return fir.loadExperimentalityForMarkerAnnotation() return fir.loadExperimentalityForMarkerAnnotation(annotatedOwnerClassName)
} }
fun FirBasedSymbol<*>.loadExperimentalitiesFromAnnotationTo(session: FirSession, result: MutableCollection<Experimentality>) { fun FirBasedSymbol<*>.loadExperimentalitiesFromAnnotationTo(session: FirSession, result: MutableCollection<Experimentality>) {
@@ -62,10 +86,15 @@ object FirOptInUsageBaseChecker {
) { ) {
for (annotation in annotations) { for (annotation in annotations) {
val annotationType = annotation.annotationTypeRef.coneTypeSafe<ConeClassLikeType>() val annotationType = annotation.annotationTypeRef.coneTypeSafe<ConeClassLikeType>()
val className = when (this) {
is FirRegularClass -> name.asString()
is FirCallableDeclaration -> symbol.callableId.className?.shortName()?.asString()
else -> null
}
result.addIfNotNull( result.addIfNotNull(
annotationType?.lookupTag?.toFirRegularClassSymbol( annotationType?.lookupTag?.toFirRegularClassSymbol(
session session
)?.loadExperimentalityForMarkerAnnotation() )?.loadExperimentalityForMarkerAnnotation(className)
) )
} }
} }
@@ -183,7 +212,8 @@ object FirOptInUsageBaseChecker {
} }
} }
private fun FirRegularClass.loadExperimentalityForMarkerAnnotation(): Experimentality? { // Note: receiver is an OptIn marker class and parameter is an annotated member owner class / self class name
private fun FirRegularClass.loadExperimentalityForMarkerAnnotation(annotatedOwnerClassName: String? = null): Experimentality? {
val experimental = getAnnotationByClassId(OptInNames.REQUIRES_OPT_IN_CLASS_ID) val experimental = getAnnotationByClassId(OptInNames.REQUIRES_OPT_IN_CLASS_ID)
?: return null ?: return null
@@ -191,7 +221,7 @@ object FirOptInUsageBaseChecker {
val levelName = levelArgument?.calleeReference?.resolved?.name?.asString() val levelName = levelArgument?.calleeReference?.resolved?.name?.asString()
val level = OptInLevel.values().firstOrNull { it.name == levelName } ?: OptInLevel.DEFAULT val level = OptInLevel.values().firstOrNull { it.name == levelName } ?: OptInLevel.DEFAULT
val message = (experimental.findArgumentByName(MESSAGE) as? FirConstExpression<*>)?.value as? String val message = (experimental.findArgumentByName(MESSAGE) as? FirConstExpression<*>)?.value as? String
return Experimentality(symbol.classId, level.severity, message) return Experimentality(symbol.classId, level.severity, message, annotatedOwnerClassName)
} }
fun reportNotAcceptedExperimentalities( fun reportNotAcceptedExperimentalities(
@@ -221,7 +251,7 @@ object FirOptInUsageBaseChecker {
context: CheckerContext, context: CheckerContext,
reporter: DiagnosticReporter reporter: DiagnosticReporter
) { ) {
for ((annotationClassId, severity, _) in experimentalities) { for ((annotationClassId, severity, markerMessage, supertypeName) in experimentalities) {
if (!symbol.fir.isExperimentalityAcceptable(annotationClassId) && if (!symbol.fir.isExperimentalityAcceptable(annotationClassId) &&
!isExperimentalityAcceptableInContext(annotationClassId, context) !isExperimentalityAcceptableInContext(annotationClassId, context)
) { ) {
@@ -229,10 +259,13 @@ object FirOptInUsageBaseChecker {
Experimentality.Severity.WARNING -> FirErrors.OPT_IN_OVERRIDE to "should" Experimentality.Severity.WARNING -> FirErrors.OPT_IN_OVERRIDE to "should"
Experimentality.Severity.ERROR -> FirErrors.OPT_IN_OVERRIDE_ERROR to "must" Experimentality.Severity.ERROR -> FirErrors.OPT_IN_OVERRIDE_ERROR to "must"
} }
val reportedMessage = "This declaration overrides experimental member of supertype " + val message = OptInNames.buildOverrideMessage(
"'${symbol.callableId.className?.shortName()?.asString()}' and $verb be annotated " + supertypeName ?: "???",
"with '@${annotationClassId.asFqNameString()}'" markerMessage,
reporter.reportOn(symbol.source, diagnostic, annotationClassId.asSingleFqName(), reportedMessage, context) verb,
markerName = annotationClassId.asFqNameString()
)
reporter.reportOn(symbol.source, diagnostic, annotationClassId.asSingleFqName(), message, context)
} }
} }
} }
@@ -480,10 +480,12 @@ class ExperimentalUsageChecker(project: Project) : CallChecker {
Experimentality.Severity.ERROR -> Errors.OPT_IN_OVERRIDE_ERROR to "must" Experimentality.Severity.ERROR -> Errors.OPT_IN_OVERRIDE_ERROR to "must"
Experimentality.Severity.FUTURE_ERROR -> Errors.OPT_IN_OVERRIDE_ERROR to "must" Experimentality.Severity.FUTURE_ERROR -> Errors.OPT_IN_OVERRIDE_ERROR to "must"
} }
val message = experimentality.message?.takeIf { it.isNotBlank() } val message = OptInNames.buildOverrideMessage(
?: "This declaration overrides experimental member of supertype " + supertypeName = member.containingDeclaration.name.asString(),
"'${member.containingDeclaration.name.asString()}' and $defaultMessageVerb be annotated " + markerMessage = experimentality.message,
"with '@${experimentality.annotationFqName.asString()}'" verb = defaultMessageVerb,
markerName = experimentality.annotationFqName.asString()
)
context.trace.report(diagnostic.on(reportOn, experimentality.annotationFqName, message)) context.trace.report(diagnostic.on(reportOn, experimentality.annotationFqName, message))
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
warning: ATTENTION! warning: ATTENTION!
This build uses in-dev FIR: This build uses in-dev FIR:
-Xuse-fir -Xuse-fir
compiler/testData/cli/jvm/optInEmptyMessage.kt:8:5: error: this declaration needs OptIn. Its usage must be marked with '@EmptyMarker' or '@OptIn(EmptyMarker::class)' compiler/testData/cli/jvm/optInEmptyMessageFir.kt:8:5: error: this declaration needs OptIn. Its usage must be marked with '@EmptyMarker' or '@OptIn(EmptyMarker::class)'
foo() foo()
^ ^
COMPILATION_ERROR COMPILATION_ERROR
+4
View File
@@ -0,0 +1,4 @@
$TESTDATA_DIR$/optInOverrideMessage.kt
-d
$TEMP_DIR$
-opt-in=kotlin.RequiresOptIn
+19
View File
@@ -0,0 +1,19 @@
@RequiresOptIn(message = "This API is experimental and can change at any time, please use with care")
annotation class Marker
@RequiresOptIn
annotation class EmptyMarker
interface Base {
@Marker
fun foo()
@EmptyMarker
fun bar()
}
class Derived : Base {
override fun foo() {}
override fun bar() {}
}
+7
View File
@@ -0,0 +1,7 @@
compiler/testData/cli/jvm/optInOverrideMessage.kt:16:18: error: base declaration of supertype 'Base' needs OptIn. This API is experimental and can change at any time, please use with care. The declaration override must be annotated with '@Marker'
override fun foo() {}
^
compiler/testData/cli/jvm/optInOverrideMessage.kt:18:18: error: base declaration of supertype 'Base' needs OptIn. The declaration override must be annotated with '@EmptyMarker'
override fun bar() {}
^
COMPILATION_ERROR
@@ -0,0 +1,5 @@
$TESTDATA_DIR$/optInOverrideMessageFir.kt
-d
$TEMP_DIR$
-opt-in=kotlin.RequiresOptIn
-Xuse-fir
+19
View File
@@ -0,0 +1,19 @@
@RequiresOptIn(message = "This API is experimental and can change at any time, please use with care")
annotation class Marker
@RequiresOptIn
annotation class EmptyMarker
interface Base {
@Marker
fun foo()
@EmptyMarker
fun bar()
}
class Derived : Base {
override fun foo() {}
override fun bar() {}
}
+10
View File
@@ -0,0 +1,10 @@
warning: ATTENTION!
This build uses in-dev FIR:
-Xuse-fir
compiler/testData/cli/jvm/optInOverrideMessageFir.kt:16:18: error: base declaration of supertype 'Base' needs OptIn. This API is experimental and can change at any time, please use with care. The declaration override must be annotated with '@Marker'
override fun foo() {}
^
compiler/testData/cli/jvm/optInOverrideMessageFir.kt:18:18: error: base declaration of supertype 'Base' needs OptIn. The declaration override must be annotated with '@EmptyMarker'
override fun bar() {}
^
COMPILATION_ERROR
@@ -809,6 +809,16 @@ public class CliTestGenerated extends AbstractCliTest {
runTest("compiler/testData/cli/jvm/optInEmptyMessageFir.args"); runTest("compiler/testData/cli/jvm/optInEmptyMessageFir.args");
} }
@TestMetadata("optInOverrideMessage.args")
public void testOptInOverrideMessage() throws Exception {
runTest("compiler/testData/cli/jvm/optInOverrideMessage.args");
}
@TestMetadata("optInOverrideMessageFir.args")
public void testOptInOverrideMessageFir() throws Exception {
runTest("compiler/testData/cli/jvm/optInOverrideMessageFir.args");
}
@TestMetadata("pluginSimple.args") @TestMetadata("pluginSimple.args")
public void testPluginSimple() throws Exception { public void testPluginSimple() throws Exception {
runTest("compiler/testData/cli/jvm/pluginSimple.args"); runTest("compiler/testData/cli/jvm/pluginSimple.args");
@@ -45,4 +45,12 @@ object OptInNames {
fun buildMessagePrefix(verb: String): String = fun buildMessagePrefix(verb: String): String =
"This declaration needs OptIn. Its usage $verb be marked" "This declaration needs OptIn. Its usage $verb be marked"
fun buildOverrideMessage(supertypeName: String, markerMessage: String?, verb: String, markerName: String): String {
val basePrefix = "Base declaration of supertype '$supertypeName' needs OptIn. "
val markerMessageOrStub = markerMessage
?.takeIf { it.isNotBlank() }?.let { if (it.endsWith(".")) "$it " else "$it. " } ?: ""
val baseSuffix = "The declaration override $verb be annotated with '@$markerName'"
return basePrefix + markerMessageOrStub + baseSuffix
}
} }