[FIR] Create fake hidden versions of List.getFirst/getLast in JDK < 21

... so that overrides are marked as deprecated regardless of the JDK.

#KT-65440 Fixed
This commit is contained in:
Kirill Rakhman
2024-02-06 13:26:18 +01:00
committed by Space Team
parent c6b2675089
commit 2f49272c42
22 changed files with 399 additions and 3 deletions
@@ -36334,6 +36334,12 @@ public class FirLightTreeOldFrontendDiagnosticsTestGenerated extends AbstractFir
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/diagnostics/tests/testsWithJava17"), Pattern.compile("^(.+)\\.kt$"), Pattern.compile("^(.+)\\.(reversed|fir|ll)\\.kts?$"), true, "multiplatform");
}
@Test
@TestMetadata("newListMethods.kt")
public void testNewListMethods() throws Exception {
runTest("compiler/testData/diagnostics/tests/testsWithJava17/newListMethods.kt");
}
@Nested
@TestMetadata("compiler/testData/diagnostics/tests/testsWithJava17/jvmRecord")
@TestDataPath("$PROJECT_ROOT")
@@ -36460,6 +36460,12 @@ public class FirPsiOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiDia
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/diagnostics/tests/testsWithJava17"), Pattern.compile("^(.*)\\.kts?$"), Pattern.compile("^(.+)\\.(reversed|fir|ll)\\.kts?$"), true, "multiplatform");
}
@Test
@TestMetadata("newListMethods.kt")
public void testNewListMethods() throws Exception {
runTest("compiler/testData/diagnostics/tests/testsWithJava17/newListMethods.kt");
}
@Nested
@TestMetadata("compiler/testData/diagnostics/tests/testsWithJava17/jvmRecord")
@TestDataPath("$PROJECT_ROOT")
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.fir.analysis.jvm
import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap
import org.jetbrains.kotlin.fir.analysis.FirOverridesBackwardCompatibilityHelper
import org.jetbrains.kotlin.fir.containingClassLookupTag
import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin
import org.jetbrains.kotlin.fir.declarations.isJavaOrEnhancement
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
@@ -19,6 +20,8 @@ import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
*/
object FirJvmOverridesBackwardCompatibilityHelper : FirOverridesBackwardCompatibilityHelper() {
override fun additionalCheck(member: FirCallableSymbol<*>): Boolean? {
if (member.origin == FirDeclarationOrigin.Synthetic.FakeHiddenInPreparationForNewJdk) return true
if (!member.isJavaOrEnhancement) return false
val containingClassName = member.containingClassLookupTag()?.classId?.asSingleFqName()?.toUnsafe() ?: return false
// If the super class is mapped to a Kotlin built-in class, then we don't require `override` keyword.
@@ -314,6 +314,7 @@ class FakeOverrideGenerator(
val baseSymbol = originalSymbol.unwrapSubstitutionAndIntersectionOverrides() as S
if (!session.visibilityChecker.isVisibleForOverriding(klass.moduleData, klass.symbol, baseSymbol.fir)) return
if (originalDeclaration.originalOrSelf().origin == FirDeclarationOrigin.Synthetic.FakeHiddenInPreparationForNewJdk) return
val (fakeOverrideFirDeclaration, baseFirSymbolsForFakeOverride) = when {
originalSymbol.shouldHaveComputedBaseSymbolsForClass(classLookupTag) -> {
@@ -229,6 +229,7 @@ class Fir2IrLazyClass(
}
private fun shouldBuildStub(fir: FirDeclaration): Boolean {
if (fir is FirCallableDeclaration && fir.originalOrSelf().origin == FirDeclarationOrigin.Synthetic.FakeHiddenInPreparationForNewJdk) return false
if (fir !is FirMemberDeclaration) return true
return when {
fir is FirConstructor -> isObject || isEnumClass || !Visibilities.isPrivate(fir.visibility) // This special case seams to be not needed anymore - KT-65172
@@ -28,7 +28,7 @@ class FirJvmDelegatedMembersFilter(private val session: FirSession) : FirDelegat
override fun shouldNotGenerateDelegatedMember(memberSymbolFromSuperInterface: FirCallableSymbol<*>): Boolean {
val original = memberSymbolFromSuperInterface.unwrapFakeOverrides()
return original.isNonAbstractJavaMethod() || original.hasJvmDefaultAnnotation() || original.isBuiltInMemberMappedToJavaDefault()
return original.isNonAbstractJavaMethod() || original.hasJvmDefaultAnnotation() || original.isBuiltInMemberMappedToJavaDefault() || original.origin == FirDeclarationOrigin.Synthetic.FakeHiddenInPreparationForNewJdk
}
// If java interface method is not abstract, then it's a default method.
@@ -7,11 +7,17 @@ package org.jetbrains.kotlin.fir.scopes.jvm
import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap
import org.jetbrains.kotlin.builtins.jvm.JvmBuiltInsSignatures
import org.jetbrains.kotlin.descriptors.EffectiveVisibility
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.fir.*
import org.jetbrains.kotlin.fir.caches.*
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.builder.buildSimpleFunction
import org.jetbrains.kotlin.fir.declarations.impl.FirResolvedDeclarationStatusImpl
import org.jetbrains.kotlin.fir.declarations.utils.classId
import org.jetbrains.kotlin.fir.declarations.utils.isFinal
import org.jetbrains.kotlin.fir.diagnostics.ConeSimpleDiagnostic
import org.jetbrains.kotlin.fir.resolve.defaultType
import org.jetbrains.kotlin.fir.resolve.isRealOwnerOf
import org.jetbrains.kotlin.fir.resolve.lookupSuperTypes
@@ -26,6 +32,8 @@ import org.jetbrains.kotlin.fir.scopes.impl.declaredMemberScope
import org.jetbrains.kotlin.fir.symbols.ConeTypeParameterLookupTag
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.types.ConeClassLikeType
import org.jetbrains.kotlin.fir.types.ConeErrorType
import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.types.impl.ConeTypeParameterTypeImpl
import org.jetbrains.kotlin.fir.types.lowerBoundIfFlexible
@@ -36,6 +44,7 @@ import org.jetbrains.kotlin.load.kotlin.SignatureBuildingComponents
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.StandardClassIds
/**
* @param firKotlinClass Kotlin version of built-in class mapped to some JDK class (e.g. kotlin.collections.List)
@@ -75,8 +84,10 @@ class JvmMappedScope(
}
}
private val isList: Boolean = firKotlinClass.classId == StandardClassIds.List
// It's ok to have a super set of actually available member names
private val myCallableNames by lazy {
private val myCallableNames: Set<Name> by lazy {
if (firKotlinClass.isFinal) {
// For final classes we don't need to load HIDDEN members at all because they might not be overridden
val signaturePrefix = firJavaClass.symbol.classId.toString()
@@ -91,6 +102,13 @@ class JvmMappedScope(
declaredMemberScope.getCallableNames() + names
} else {
declaredMemberScope.getCallableNames() + javaMappedClassUseSiteScope.getCallableNames()
}.let {
// If getFirst/getLast don't exist, we need to add them so that we can mark overrides as deprecated (KT-65440)
if (isList && (GET_FIRST_NAME !in it || GET_LAST_NAME !in it)) {
it + listOf(GET_FIRST_NAME, GET_LAST_NAME)
} else {
it
}
}
}
@@ -112,6 +130,8 @@ class JvmMappedScope(
}
}
var needsHiddenFake = isList && (name == GET_FIRST_NAME || name == GET_LAST_NAME)
javaMappedClassUseSiteScope.processFunctionsByName(name) processor@{ symbol ->
if (!symbol.isDeclaredInMappedJavaClass() || !(symbol.fir.status as FirResolvedDeclarationStatus).visibility.isPublicAPI) {
return@processor
@@ -137,8 +157,46 @@ class JvmMappedScope(
if ((jdkMemberStatus == JDKMemberStatus.HIDDEN || jdkMemberStatus == JDKMemberStatus.HIDDEN_IN_DECLARING_CLASS_ONLY) && firKotlinClass.isFinal) return@processor
val newSymbol = mappedSymbolCache.mappedFunctions.getValue(symbol, this to jdkMemberStatus)
if (needsHiddenFake &&
allJavaMappedSuperClassIds.any {
SignatureBuildingComponents.signature(it, jvmDescriptor) in JvmBuiltInsSignatures.DEPRECATED_LIST_METHODS
}
) {
needsHiddenFake = false
}
processor(newSymbol)
}
if (needsHiddenFake) {
// We're in JDK < 21, i.e., getFirst/getLast don't exist in the List interface yet.
// We create a fake version of them for the sole purpose of reporting deprecations on to-become-overrides like in LinkedList.
// This is because we want to rename these two methods in the future,
// and we want to warn users of older JDKs of a potential breaking change caused by upgrading to JDK >= 21.
// See KT-65440.
val fakeSymbol = mappedSymbolCache.hiddenFakeFunctions.getValue(name, this)
processor(fakeSymbol)
}
}
private fun createHiddenFakeFunction(name: Name): FirNamedFunctionSymbol {
return buildSimpleFunction {
moduleData = firKotlinClass.moduleData
origin = FirDeclarationOrigin.Synthetic.FakeHiddenInPreparationForNewJdk
status = FirResolvedDeclarationStatusImpl(Visibilities.Public, Modality.OPEN, EffectiveVisibility.Public)
returnTypeRef = buildResolvedTypeRef {
type = firKotlinClass.typeParameters.firstOrNull()
?.let { ConeTypeParameterTypeImpl(it.symbol.toLookupTag(), isNullable = false) }
?: ConeErrorType(ConeSimpleDiagnostic("No type parameter found on '${firKotlinClass.classKind}'"))
}
this.name = name
dispatchReceiverType = firKotlinClass.defaultType()
symbol = FirNamedFunctionSymbol(CallableId(firKotlinClass.classId, name))
resolvePhase = FirResolvePhase.BODY_RESOLVE
}.apply {
isHiddenEverywhereBesideSuperCalls = HiddenEverywhereBesideSuperCallsStatus.HIDDEN_FAKE
}.symbol
}
private fun isOverrideOfKotlinDeclaredFunction(symbol: FirNamedFunctionSymbol) =
@@ -335,6 +393,11 @@ class JvmMappedScope(
scope.createMappedFunction(symbol, jdkMemberStatus)
}
val hiddenFakeFunctions: FirCache<Name, FirNamedFunctionSymbol, JvmMappedScope> =
cachesFactory.createCache { name, scope ->
scope.createHiddenFakeFunction(name)
}
val mappedConstructors: FirCache<FirConstructorSymbol, FirConstructorSymbol, JvmMappedScope> =
cachesFactory.createCache { symbol, scope ->
scope.createMappedConstructor(symbol)
@@ -357,6 +420,9 @@ class JvmMappedScope(
},
session
)
private val GET_FIRST_NAME = Name.identifier("getFirst")
private val GET_LAST_NAME = Name.identifier("getLast")
}
override fun toString(): String {
@@ -331,7 +331,7 @@ var FirCallableDeclaration.isHiddenEverywhereBesideSuperCalls: HiddenEverywhereB
)
enum class HiddenEverywhereBesideSuperCallsStatus {
HIDDEN, HIDDEN_IN_DECLARING_CLASS_ONLY
HIDDEN, HIDDEN_IN_DECLARING_CLASS_ONLY, HIDDEN_FAKE,
}
private object IsHiddenToOvercomeSignatureClash : FirDeclarationDataKey()
@@ -360,5 +360,7 @@ fun FirCallableSymbol<*>.isHidden(isSuperCall: Boolean, isOverridden: Boolean):
// we report a deprecation warning on super calls because we might want to rename the method in the future
// (getFirst -> first).
HiddenEverywhereBesideSuperCallsStatus.HIDDEN_IN_DECLARING_CLASS_ONLY -> if (isSuperCall || isOverridden) CallToPotentiallyHiddenSymbolResult.VisibleWithDeprecation else CallToPotentiallyHiddenSymbolResult.Hidden
// HIDDEN_FAKE is always hidden (even for super calls), unless overridden.
HiddenEverywhereBesideSuperCallsStatus.HIDDEN_FAKE -> if (isOverridden) CallToPotentiallyHiddenSymbolResult.VisibleWithDeprecation else CallToPotentiallyHiddenSymbolResult.Hidden
}
}
@@ -35,6 +35,7 @@ sealed class FirDeclarationOrigin(
object FakeFunction : Synthetic()
object ForwardDeclaration : Synthetic()
object ScriptTopLevelDestructuringDeclarationContainer : Synthetic()
object FakeHiddenInPreparationForNewJdk : Synthetic()
}
object DynamicScope : FirDeclarationOrigin()
object SamConstructor : FirDeclarationOrigin()
@@ -0,0 +1,29 @@
import java.util.LinkedList
class A<T> : ArrayList<T>() {
fun getFirst(): T = super.<!UNRESOLVED_REFERENCE!>getFirst<!>()
fun getLast(): T = super.<!UNRESOLVED_REFERENCE!>getLast<!>()
}
fun foo(x: List<String>, y: LinkedList<String>, z: A<String>) {
x.<!UNRESOLVED_REFERENCE!>getFirst<!>()
x.<!FUNCTION_CALL_EXPECTED!>first<!>
x.first() // stdlib extension on List
x.<!UNRESOLVED_REFERENCE!>getLast<!>()
x.<!FUNCTION_CALL_EXPECTED!>last<!>
x.last()
y.<!DEPRECATION!>getFirst<!>()
y.<!DEPRECATION!>first<!>
y.first()
y.<!DEPRECATION!>getLast<!>()
y.<!DEPRECATION!>last<!>
y.last()
z.<!DEPRECATION!>getFirst<!>()
z.<!FUNCTION_CALL_EXPECTED!>first<!>
z.first()
z.<!DEPRECATION!>getLast<!>()
z.<!FUNCTION_CALL_EXPECTED!>last<!>
z.last()
}
@@ -0,0 +1,29 @@
import java.util.LinkedList
class A<T> : ArrayList<T>() {
fun getFirst(): T = super.<!UNRESOLVED_REFERENCE!>getFirst<!>()
fun getLast(): T = super.<!UNRESOLVED_REFERENCE!>getLast<!>()
}
fun foo(x: List<String>, y: LinkedList<String>, z: A<String>) {
x.<!UNRESOLVED_REFERENCE!>getFirst<!>()
x.<!FUNCTION_CALL_EXPECTED!>first<!>
x.first() // stdlib extension on List
x.<!UNRESOLVED_REFERENCE!>getLast<!>()
x.<!FUNCTION_CALL_EXPECTED!>last<!>
x.last()
y.getFirst()
y.first
y.first()
y.getLast()
y.last
y.last()
z.getFirst()
z.<!FUNCTION_CALL_EXPECTED!>first<!>
z.first()
z.getLast()
z.<!FUNCTION_CALL_EXPECTED!>last<!>
z.last()
}
@@ -38708,6 +38708,12 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/diagnostics/tests/testsWithJava17"), Pattern.compile("^(.*)\\.kts?$"), Pattern.compile("^(.+)\\.(reversed|fir|ll)\\.kts?$"), true);
}
@Test
@TestMetadata("newListMethods.kt")
public void testNewListMethods() throws Exception {
runTest("compiler/testData/diagnostics/tests/testsWithJava17/newListMethods.kt");
}
@Nested
@TestMetadata("compiler/testData/diagnostics/tests/testsWithJava17/jvmRecord")
@TestDataPath("$PROJECT_ROOT")