diff --git a/arrow-libs/optics/arrow-optics-compiler-plugin/build.gradle.kts b/arrow-libs/optics/arrow-optics-compiler-plugin/build.gradle.kts index 33340cfd71..b75a073e62 100644 --- a/arrow-libs/optics/arrow-optics-compiler-plugin/build.gradle.kts +++ b/arrow-libs/optics/arrow-optics-compiler-plugin/build.gradle.kts @@ -35,6 +35,44 @@ dependencies { testRuntimeOnly(projects.arrowOptics) } +// Compilers the tests also run on, each with its own test task in `check`. The default `test` task runs on the +// compiler kotlin-compile-testing depends on. The tests and plugin are compiled once; each compiler only changes +// the runtime classpath, so these runs check binary compatibility. +val testCompilers = listOf("2.5.0-Beta2") + +testCompilers.forEach { version -> + val suffix = "Kotlin" + version.replace(Regex("[^A-Za-z0-9]"), "_") + val testRuntimeClasspath = configurations.testRuntimeClasspath.get() + val runtimeClasspath = configurations.create("test${suffix}RuntimeClasspath") { + isCanBeConsumed = false + extendsFrom(testRuntimeClasspath) + // replicate testRuntimeClasspath's attributes for the same variant resolution + @Suppress("UnstableApiUsage") + attributes.addAllLater(testRuntimeClasspath.attributes) + resolutionStrategy.eachDependency { + if (requested.group == "org.jetbrains.kotlin" && requested.name.startsWith("kotlin-")) useVersion(version) + } + } + val testTask = tasks.register("test$suffix") { + description = "Runs the tests on Kotlin $version." + group = LifecycleBasePlugin.VERIFICATION_GROUP + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().output + sourceSets.main.get().output + runtimeClasspath + // Kotlin 2.5+ needs JDK 17 to run the compiler. + javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(17)) }) + } + tasks.check { dependsOn(testTask) } +} + +// Kotlin 2.5 reflects over its own classes' fields and fails on the field coverage instrumentation adds. +kover { + currentProject { + instrumentation { + excludedClasses.add("org.jetbrains.kotlin.*") + } + } +} + tasks.withType().configureEach { maxParallelForks = 1 useJUnitPlatform() diff --git a/arrow-libs/optics/arrow-optics-compiler-plugin/src/main/kotlin/arrow/optics/plugin/fir/FirOpticsExtractor.kt b/arrow-libs/optics/arrow-optics-compiler-plugin/src/main/kotlin/arrow/optics/plugin/fir/FirOpticsExtractor.kt index 6ded6145b5..63c035334e 100644 --- a/arrow-libs/optics/arrow-optics-compiler-plugin/src/main/kotlin/arrow/optics/plugin/fir/FirOpticsExtractor.kt +++ b/arrow-libs/optics/arrow-optics-compiler-plugin/src/main/kotlin/arrow/optics/plugin/fir/FirOpticsExtractor.kt @@ -43,8 +43,10 @@ import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirTypeParameterSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirValueParameterSymbol import org.jetbrains.kotlin.fir.symbols.lazyResolveToPhase +import org.jetbrains.kotlin.fir.types.ConeClassifierLookupTag import org.jetbrains.kotlin.fir.types.ConeDefinitelyNotNullType import org.jetbrains.kotlin.fir.types.ConeKotlinType +import org.jetbrains.kotlin.fir.types.ConeLookupTagBasedType import org.jetbrains.kotlin.fir.types.ConeStarProjection import org.jetbrains.kotlin.fir.types.ConeTypeParameterType import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef @@ -242,7 +244,9 @@ object FirOpticsExtractor { */ private fun sealedLensFoci(symbol: FirRegularClassSymbol, session: FirSession, resolveFocusTypes: Boolean): List { val abstractProps = mutableListOf() - symbol.processAllDeclarations(session) visitor@{ + // Members are only needed at RAW_FIR here. The default (STATUS) breaks in the IDE when resolving lazily. + // Only names and syntactic modifiers are read early; return types and visibility are resolved on demand only when computing focus types. + symbol.processAllDeclarations(session, memberRequiredPhase = FirResolvePhase.RAW_FIR) visitor@{ if (it !is FirPropertySymbol) return@visitor if (!it.isAbstractMemberOf(symbol)) return@visitor if (it.receiverParameterSymbol != null) return@visitor @@ -350,7 +354,15 @@ object FirOpticsExtractor { private fun ConeKotlinType.mentions(param: FirTypeParameterSymbol, session: FirSession): Boolean = contains { (it as? ConeTypeParameterType)?.toTypeParameterSymbol(session) == param } - fun ConeTypeParameterType.toTypeParameterSymbol(session: FirSession): FirTypeParameterSymbol? = lookupTag.toSymbol(session) as? FirTypeParameterSymbol + fun ConeTypeParameterType.toTypeParameterSymbol(session: FirSession): FirTypeParameterSymbol? = stableLookupTag.toSymbol(session) as? FirTypeParameterSymbol + + /** + * The lookup tag, read through the supertype: `ConeTypeParameterType.lookupTag` returns a class that moved + * package in Kotlin 2.5, so calling it directly links on only one of 2.4 and 2.5; the supertype's getter + * (`ConeLookupTagBasedType.lookupTag`) has the same signature in both. + */ + @Suppress("USELESS_CAST") + private val ConeTypeParameterType.stableLookupTag: ConeClassifierLookupTag get() = (this as ConeLookupTagBasedType).lookupTag /** * Whether [this] property is abstract. Interface members without a body are implicitly abstract, but @@ -381,7 +393,7 @@ object FirOpticsExtractor { private fun sameType(a: ConeKotlinType, b: ConeKotlinType): Boolean { if (a.isMarkedNullable != b.isMarkedNullable) return false if (a is ConeTypeParameterType || b is ConeTypeParameterType) { - return a is ConeTypeParameterType && b is ConeTypeParameterType && a.lookupTag == b.lookupTag + return a is ConeTypeParameterType && b is ConeTypeParameterType && a.stableLookupTag == b.stableLookupTag } if (a is ConeDefinitelyNotNullType || b is ConeDefinitelyNotNullType) { return a is ConeDefinitelyNotNullType && b is ConeDefinitelyNotNullType && sameType(a.original, b.original)