From ff0c3e36899180a780dd8ddebd4b5adca4de7885 Mon Sep 17 00:00:00 2001 From: Phil Davies Date: Tue, 27 Oct 2026 20:00:01 +0000 Subject: [PATCH 1/3] fix(arrow-optics-compiler-plugin): don't resolve sealed members past RAW_FIR sealedLensFoci used processAllDeclarations with its default member phase (STATUS). In the IDE, the DSL's top-level names can be requested while a class is still in COMPANION_GENERATION (e.g. by kotlinx-serialization), and lazy resolution rejects the jump, failing with "Error while resolving FirRegularClassImpl from COMPILER_REQUIRED_ANNOTATIONS to STATUS". --- .../main/kotlin/arrow/optics/plugin/fir/FirOpticsExtractor.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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..06aa1f9651 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 @@ -242,7 +242,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 From 87bbd74a68d157d7eee20228bb8254fde7e3cabd Mon Sep 17 00:00:00 2001 From: Phil Davies Date: Tue, 27 Oct 2026 20:01:20 +0000 Subject: [PATCH 2/3] fix(arrow-optics-compiler-plugin): avoid lookupTag return type mismatch on K2.5 Use the supertype's `lookupTag` return type to avoid hitting the moved class on K2.5. --- .../arrow/optics/plugin/fir/FirOpticsExtractor.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 06aa1f9651..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 @@ -352,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 @@ -383,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) From d8aa150a1c9723a5abae2237a4a5fb29bbe16b8c Mon Sep 17 00:00:00 2001 From: Phil Davies Date: Tue, 27 Oct 2026 20:23:34 +0000 Subject: [PATCH 3/3] build(arrow-optics-compiler-plugin): add additional compiler test runs Run the test suite against each additional compiler through runtime classpath only. --- .../build.gradle.kts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) 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()