Report diagnostic on overrides of experimental members

Unless they're experimental themselves

 #KT-22759 In Progress
This commit is contained in:
Alexander Udalov
2018-01-03 15:02:19 +01:00
parent 29c35e6686
commit 153c86c069
12 changed files with 382 additions and 17 deletions
@@ -229,6 +229,9 @@ public interface Errors {
DiagnosticFactory2<PsiElement, FqName, Boolean> EXPERIMENTAL_API_USAGE = DiagnosticFactory2.create(WARNING);
DiagnosticFactory2<PsiElement, FqName, Boolean> EXPERIMENTAL_API_USAGE_ERROR = DiagnosticFactory2.create(ERROR);
DiagnosticFactory2<PsiElement, FqName, DeclarationDescriptor> EXPERIMENTAL_OVERRIDE = DiagnosticFactory2.create(WARNING);
DiagnosticFactory2<PsiElement, FqName, DeclarationDescriptor> EXPERIMENTAL_OVERRIDE_ERROR = DiagnosticFactory2.create(ERROR);
DiagnosticFactory0<KtAnnotationEntry> USE_EXPERIMENTAL_WITHOUT_ARGUMENTS = DiagnosticFactory0.create(WARNING);
DiagnosticFactory1<KtAnnotationEntry, FqName> USE_EXPERIMENTAL_ARGUMENT_IS_NOT_MARKER = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<KtAnnotationEntry, FqName> USE_EXPERIMENTAL_ARGUMENT_HAS_NON_COMPILATION_IMPACT = DiagnosticFactory1.create(ERROR);
@@ -151,6 +151,9 @@ public class DefaultErrorMessages {
MAP.put(EXPERIMENTAL_API_USAGE, "This declaration is experimental and its usage should be marked with ''@{0}''{1}", TO_STRING, renderUseExperimental);
MAP.put(EXPERIMENTAL_API_USAGE_ERROR, "This declaration is experimental and its usage must be marked with ''@{0}''{1}", TO_STRING, renderUseExperimental);
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(USE_EXPERIMENTAL_WITHOUT_ARGUMENTS, "@UseExperimental without any arguments has no effect");
MAP.put(USE_EXPERIMENTAL_ARGUMENT_IS_NOT_MARKER, "Annotation ''{0}'' is not an experimental API marker, therefore its usage in @UseExperimental is ignored", TO_STRING);
MAP.put(USE_EXPERIMENTAL_ARGUMENT_HAS_NON_COMPILATION_IMPACT,
@@ -81,7 +81,8 @@ private val DEFAULT_DECLARATION_CHECKERS = listOf(
DelegationChecker(),
KClassWithIncorrectTypeArgumentChecker,
SuspendOperatorsCheckers,
InlineClassDeclarationChecker
InlineClassDeclarationChecker,
ExperimentalUsageChecker.Overrides
)
private val DEFAULT_CALL_CHECKERS = listOf(
@@ -24,7 +24,6 @@ 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.BindingTrace
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
@@ -70,15 +69,32 @@ object ExperimentalUsageChecker : CallChecker {
}
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
checkExperimental(resolvedCall.resultingDescriptor, reportOn, context.trace, context.moduleDescriptor)
checkExperimental(resolvedCall.resultingDescriptor, reportOn, context)
}
private fun checkExperimental(descriptor: DeclarationDescriptor, element: PsiElement, trace: BindingTrace, module: ModuleDescriptor) {
private fun checkExperimental(descriptor: DeclarationDescriptor, element: PsiElement, context: CheckerContext) {
val experimentalities = descriptor.loadExperimentalities()
if (experimentalities.isEmpty()) return
if (experimentalities.isNotEmpty()) {
checkExperimental(experimentalities, element, context.trace.bindingContext, 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))
}
}
}
val isBodyUsageExceptPublicInline = element.isBodyUsage(trace.bindingContext, allowPublicInline = false)
val isBodyUsage = isBodyUsageExceptPublicInline || element.isBodyUsage(trace.bindingContext, allowPublicInline = true)
private fun checkExperimental(
experimentalities: Collection<Experimentality>,
element: PsiElement,
bindingContext: BindingContext,
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 =
@@ -87,20 +103,14 @@ object ExperimentalUsageChecker : CallChecker {
val isBodyUsageInSameModule =
experimentality.markerDescriptor.module == module && isBodyUsageExceptPublicInline
val annotationFqName = experimentality.annotationFqName
val isExperimentalityAccepted =
isBodyUsageInSameModule ||
(isBodyUsageOfCompilationExperimentality &&
element.hasContainerAnnotatedWithUseExperimental(annotationFqName, trace.bindingContext)) ||
element.propagates(annotationFqName, trace.bindingContext)
element.hasContainerAnnotatedWithUseExperimental(experimentality.annotationFqName, bindingContext)) ||
element.propagates(experimentality.annotationFqName, bindingContext)
if (!isExperimentalityAccepted) {
val diagnostic = when (experimentality.severity) {
ExperimentalUsageChecker.Experimentality.Severity.WARNING -> Errors.EXPERIMENTAL_API_USAGE
ExperimentalUsageChecker.Experimentality.Severity.ERROR -> Errors.EXPERIMENTAL_API_USAGE_ERROR
}
trace.report(diagnostic.on(element, annotationFqName, isBodyUsageOfCompilationExperimentality))
report(experimentality, isBodyUsageOfCompilationExperimentality)
}
}
}
@@ -214,7 +224,30 @@ object ExperimentalUsageChecker : CallChecker {
object ClassifierUsage : ClassifierUsageChecker {
override fun check(targetDescriptor: ClassifierDescriptor, element: PsiElement, context: ClassifierUsageCheckerContext) {
checkExperimental(targetDescriptor, element, context.trace, context.moduleDescriptor)
checkExperimental(targetDescriptor, element, context)
}
}
object Overrides : DeclarationChecker {
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 }
}.toMap()
val module = descriptor.module
for ((experimentality, member) in experimentalOverridden) {
checkExperimental(listOf(experimentality), declaration, context.trace.bindingContext, module) { _, _ ->
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))
}
}
}
}
@@ -0,0 +1,28 @@
// !API_VERSION: 1.3
// MODULE: api
// FILE: api.kt
package api
@Experimental(Experimental.Level.ERROR, [Experimental.Impact.COMPILATION])
annotation class E
open class Base {
@E
open fun foo() {}
}
// MODULE: usage(api)
// FILE: usage.kt
package usage
import api.*
class Derived : Base() {
override fun <!EXPERIMENTAL_OVERRIDE_ERROR!>foo<!>() {}
}
fun test(b: Base) {
b.<!EXPERIMENTAL_API_USAGE_ERROR!>foo<!>()
}
@@ -0,0 +1,39 @@
// -- Module: <api> --
package
package api {
public open class Base {
public constructor Base()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@api.E public open fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.Experimental(changesMayBreak = {Impact.COMPILATION}, level = Level.ERROR) public final annotation class E : kotlin.Annotation {
public constructor E()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
// -- Module: <usage> --
package
package api {
}
package usage {
public fun test(/*0*/ b: api.Base): kotlin.Unit
public final class Derived : api.Base {
public constructor Derived()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
@@ -0,0 +1,47 @@
// !API_VERSION: 1.3
// MODULE: api
// FILE: api.kt
package api
@Experimental(Experimental.Level.WARNING, [Experimental.Impact.COMPILATION])
annotation class E
open class Base {
@E
open fun foo() {}
}
class DerivedInSameModule : Base() {
override fun <!EXPERIMENTAL_OVERRIDE!>foo<!>() {}
}
// MODULE: usage1(api)
// FILE: usage-propagate.kt
package usage1
import api.*
open class Derived : Base() {
@E
override fun foo() {}
}
class SubDerived : Derived()
@E
class Derived2 : Base() {
override fun foo() {}
}
// MODULE: usage2(api)
// FILE: usage-none.kt
package usage2
import api.*
class Derived : Base() {
override fun <!EXPERIMENTAL_OVERRIDE!>foo<!>() {}
}
@@ -0,0 +1,80 @@
// -- Module: <api> --
package
package api {
public open class Base {
public constructor Base()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@api.E public open fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public final class DerivedInSameModule : api.Base {
public constructor DerivedInSameModule()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.Experimental(changesMayBreak = {Impact.COMPILATION}, level = Level.WARNING) public final annotation class E : kotlin.Annotation {
public constructor E()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
// -- Module: <usage1> --
package
package api {
}
package usage1 {
public open class Derived : api.Base {
public constructor Derived()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@api.E public open override /*1*/ fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@api.E public final class Derived2 : api.Base {
public constructor Derived2()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public final class SubDerived : usage1.Derived {
public constructor SubDerived()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@api.E public open override /*1*/ /*fake_override*/ fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
// -- Module: <usage2> --
package
package api {
}
package usage2 {
public final class Derived : api.Base {
public constructor Derived()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
@@ -0,0 +1,35 @@
// !API_VERSION: 1.3
@Experimental(Experimental.Level.WARNING)
annotation class E1
@Experimental(Experimental.Level.WARNING)
annotation class E3
interface Base1 {
@E1
fun foo()
}
interface Base2 {
fun foo()
}
interface Base3 {
@E3
fun foo()
}
class DerivedA : Base1, Base2, Base3 {
override fun <!EXPERIMENTAL_OVERRIDE, EXPERIMENTAL_OVERRIDE!>foo<!>() {}
}
class DerivedB : Base1, Base3 {
@E3
override fun <!EXPERIMENTAL_OVERRIDE!>foo<!>() {}
}
class DerivedC : Base1, Base2, Base3 {
@E1
@E3
override fun foo() {}
}
@@ -0,0 +1,60 @@
package
public interface Base1 {
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@E1 public abstract fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public interface Base2 {
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public abstract fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public interface Base3 {
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@E3 public abstract fun foo(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public final class DerivedA : Base1, Base2, Base3 {
public constructor DerivedA()
public open override /*3*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*3*/ fun foo(): kotlin.Unit
public open override /*3*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*3*/ /*fake_override*/ fun toString(): kotlin.String
}
public final class DerivedB : Base1, Base3 {
public constructor DerivedB()
public open override /*2*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@E3 public open override /*2*/ fun foo(): kotlin.Unit
public open override /*2*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*2*/ /*fake_override*/ fun toString(): kotlin.String
}
public final class DerivedC : Base1, Base2, Base3 {
public constructor DerivedC()
public open override /*3*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
@E1 @E3 public open override /*3*/ fun foo(): kotlin.Unit
public open override /*3*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*3*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.Experimental(level = Level.WARNING) public final annotation class E1 : kotlin.Annotation {
public constructor E1()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.Experimental(level = Level.WARNING) public final annotation class E3 : kotlin.Annotation {
public constructor E3()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@@ -1839,6 +1839,12 @@ public class DiagnosticsTestWithStdLibGenerated extends AbstractDiagnosticsTestW
doTest(fileName);
}
@TestMetadata("errors.kt")
public void testErrors() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/errors.kt");
doTest(fileName);
}
@TestMetadata("experimentalWithNoImpact.kt")
public void testExperimentalWithNoImpact() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalWithNoImpact.kt");
@@ -1863,6 +1869,18 @@ public class DiagnosticsTestWithStdLibGenerated extends AbstractDiagnosticsTestW
doTest(fileName);
}
@TestMetadata("override.kt")
public void testOverride() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/override.kt");
doTest(fileName);
}
@TestMetadata("overrideDifferentExperimentalities.kt")
public void testOverrideDifferentExperimentalities() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/overrideDifferentExperimentalities.kt");
doTest(fileName);
}
@TestMetadata("topLevel.kt")
public void testTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/topLevel.kt");
@@ -1839,6 +1839,12 @@ public class DiagnosticsTestWithStdLibUsingJavacGenerated extends AbstractDiagno
doTest(fileName);
}
@TestMetadata("errors.kt")
public void testErrors() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/errors.kt");
doTest(fileName);
}
@TestMetadata("experimentalWithNoImpact.kt")
public void testExperimentalWithNoImpact() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/experimentalWithNoImpact.kt");
@@ -1863,6 +1869,18 @@ public class DiagnosticsTestWithStdLibUsingJavacGenerated extends AbstractDiagno
doTest(fileName);
}
@TestMetadata("override.kt")
public void testOverride() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/override.kt");
doTest(fileName);
}
@TestMetadata("overrideDifferentExperimentalities.kt")
public void testOverrideDifferentExperimentalities() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/overrideDifferentExperimentalities.kt");
doTest(fileName);
}
@TestMetadata("topLevel.kt")
public void testTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/experimental/topLevel.kt");