diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9f83d306a..144bfe91565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.26.7 + +* `Versions`: + * `Kotlin`: `2.2.20` -> `2.2.21` + * `Compose`: `1.8.2` -> `1.9.2` + * `KSP`: `2.2.20-2.0.3` -> `2.3.1` +* `Coroutines`: + * Fix `SmartSemaphore.waitRelease` to wait for the exact number of permits + * Improve `SmartKeyRWLocker` tests +* `KSP`: + * `Sealed`/`ClassCasts`/`Variations`: + * Add workaround for `NoSuchElementException` to improve processors stability on new `KSP` +* `Koin`: + * `Generator`: + * Handle missing annotation values safely (`NoSuchElementException` workaround) +* `Android`: + * `Pickers`: + * Add dependency `androidx.compose.material:material-icons-extended` + ## 0.26.6 * `Versions`: diff --git a/android/pickers/build.gradle b/android/pickers/build.gradle index 17c6ce042b4..faacb584ba8 100644 --- a/android/pickers/build.gradle +++ b/android/pickers/build.gradle @@ -13,6 +13,7 @@ kotlin { androidMain { dependencies { api project(":micro_utils.android.smalltextfield") + api libs.jb.compose.icons } } } diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt index c557037a3b5..eb06cafd9c7 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt @@ -72,7 +72,7 @@ sealed interface SmartSemaphore { acquiredPermits != checkedPermits } if (shouldContinue) { - waitRelease() + waitRelease(checkedPermits - acquiredPermits) } } while (shouldContinue && currentCoroutineContext().isActive) } catch (e: Throwable) { diff --git a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt index 70038c189b4..a0906812b28 100644 --- a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt +++ b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class SmartKeyRWLockerTests { - @Test - fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" - locker.lockWrite() + private lateinit var locker: SmartKeyRWLocker - assertTrue { locker.isWriteLocked() } - - assertFails { - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) - } - } - assertFalse { locker.isWriteLocked(testKey) } - - locker.unlockWrite() - assertFalse { locker.isWriteLocked() } - - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) - } - assertTrue { locker.isWriteLocked(testKey) } - assertTrue { locker.unlockWrite(testKey) } - assertFalse { locker.isWriteLocked(testKey) } + @BeforeTest + fun setup() { + locker = SmartKeyRWLocker() } + + // ==================== Global Read Tests ==================== + @Test - fun writeLockKeyFailedOnGlobalReadLockTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" + fun testGlobalReadAllowsMultipleConcurrentReads() = runTest { + val results = mutableListOf() + locker.acquireRead() - assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits) - - assertFails { - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) + val jobs = List(5) { + launch { + locker.acquireRead() + delay(100.milliseconds) + results.add(true) + locker.releaseRead() } } - assertFalse { locker.isWriteLocked(testKey) } + + jobs.joinAll() + locker.releaseRead() + + assertEquals(5, results.size) + } + + @Test + fun testGlobalReadBlocksGlobalWrite() = runTest { + locker.acquireRead() + + var writeAcquired = false + val writeJob = launch { + locker.lockWrite() + writeAcquired = true + locker.unlockWrite() + } + + delay(200.milliseconds) + assertFalse(writeAcquired, "Write should be blocked by global read") locker.releaseRead() - assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits) + writeJob.join() - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) - } - assertTrue { locker.isWriteLocked(testKey) } - assertTrue { locker.unlockWrite(testKey) } - assertFalse { locker.isWriteLocked(testKey) } + assertTrue(writeAcquired, "Write should succeed after read released") } + @Test - fun readLockFailedOnWriteLockKeyTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" - locker.lockWrite(testKey) + fun testGlobalReadBlocksAllKeyWrites() = runTest { + locker.acquireRead() - assertTrue { locker.isWriteLocked(testKey) } + val writeFlags = mutableMapOf() + val keys = listOf("key1", "key2", "key3") - assertFails { - realWithTimeout(1.seconds) { + val jobs = keys.map { key -> + launch { + locker.lockWrite(key) + writeFlags[key] = true + locker.unlockWrite(key) + } + } + + delay(200.milliseconds) + assertTrue(writeFlags.isEmpty(), "No writes should succeed while global read active") + + locker.releaseRead() + jobs.joinAll() + + assertEquals(keys.size, writeFlags.size, "All writes should succeed after global read released") + } + + // ==================== Global Write Tests ==================== + + @Test + fun testGlobalWriteBlocksAllOperations() = runTest { + locker.lockWrite() + + var globalReadAcquired = false + var keyReadAcquired = false + var keyWriteAcquired = false + + val jobs = listOf( + launch { locker.acquireRead() + globalReadAcquired = true + locker.releaseRead() + }, + launch { + locker.acquireRead("key1") + keyReadAcquired = true + locker.releaseRead("key1") + }, + launch { + locker.lockWrite("key2") + keyWriteAcquired = true + locker.unlockWrite("key2") } - } - assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) + ) - locker.unlockWrite(testKey) - assertFalse { locker.isWriteLocked(testKey) } + delay(200.milliseconds) + assertFalse(globalReadAcquired, "Global read should be blocked") + assertFalse(keyReadAcquired, "Key read should be blocked") + assertFalse(keyWriteAcquired, "Key write should be blocked") - realWithTimeout(1.seconds) { - locker.acquireRead() - } - assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) - assertTrue { locker.releaseRead() } - assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits) + locker.unlockWrite() + jobs.joinAll() + + assertTrue(globalReadAcquired) + assertTrue(keyReadAcquired) + assertTrue(keyWriteAcquired) } + @Test - fun writeLockFailedOnWriteLockKeyTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" - locker.lockWrite(testKey) + fun testGlobalWriteIsExclusive() = runTest { + locker.lockWrite() - assertTrue { locker.isWriteLocked(testKey) } - - assertFails { - realWithTimeout(1.seconds) { - locker.lockWrite() - } - } - assertFalse(locker.isWriteLocked()) - - locker.unlockWrite(testKey) - assertFalse { locker.isWriteLocked(testKey) } - - realWithTimeout(1.seconds) { + var secondWriteAcquired = false + val job = launch { locker.lockWrite() + secondWriteAcquired = true + locker.unlockWrite() } - assertTrue(locker.isWriteLocked()) - assertTrue { locker.unlockWrite() } - assertFalse(locker.isWriteLocked()) + + delay(200.milliseconds) + assertFalse(secondWriteAcquired, "Second global write should be blocked") + + locker.unlockWrite() + job.join() + + assertTrue(secondWriteAcquired) } + + // ==================== Key Read Tests ==================== + @Test - fun readsBlockingGlobalWrite() = runTest { - val locker = SmartKeyRWLocker() + fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest { + val key = "testKey" + val results = mutableListOf() - val testKeys = (0 until 100).map { "test$it" } + locker.acquireRead(key) - for (i in testKeys.indices) { - val it = testKeys[i] - locker.acquireRead(it) - val previous = testKeys.take(i) - val next = testKeys.drop(i + 1) - - previous.forEach { - assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 } - } - next.forEach { - assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null } + val jobs = List(5) { + launch { + locker.acquireRead(key) + delay(50.milliseconds) + results.add(true) + locker.releaseRead(key) } } - for (i in testKeys.indices) { - val it = testKeys[i] - assertFails { - realWithTimeout(13.milliseconds) { locker.lockWrite() } + jobs.joinAll() + locker.releaseRead(key) + + assertEquals(5, results.size) + } + + @Test + fun testKeyReadAllowsReadsForDifferentKeys() = runTest { + val results = mutableMapOf() + + locker.acquireRead("key1") + + val jobs = listOf("key2", "key3", "key4").map { key -> + launch { + locker.acquireRead(key) + delay(50.milliseconds) + results[key] = true + locker.releaseRead(key) } - val readPermitsBeforeLock = locker.readSemaphore().freePermits - realWithTimeout(1.seconds) { locker.acquireRead() } + } + + jobs.joinAll() + locker.releaseRead("key1") + + assertEquals(3, results.size) + } + + @Test + fun testKeyReadBlocksWriteForSameKey() = runTest { + val key = "testKey" + locker.acquireRead(key) + + var writeAcquired = false + val job = launch { + locker.lockWrite(key) + writeAcquired = true + locker.unlockWrite(key) + } + + delay(200.milliseconds) + assertFalse(writeAcquired, "Write for same key should be blocked") + + locker.releaseRead(key) + job.join() + + assertTrue(writeAcquired) + } + + @Test + fun testKeyReadBlocksGlobalWrite() = runTest { + locker.acquireRead("key1") + + var globalWriteAcquired = false + val job = launch { + locker.lockWrite() + globalWriteAcquired = true + locker.unlockWrite() + } + + delay(200.milliseconds) + assertFalse(globalWriteAcquired, "Global write should be blocked by key read") + + locker.releaseRead("key1") + job.join() + + assertTrue(globalWriteAcquired) + } + + @Test + fun testKeyReadAllowsWriteForDifferentKey() = runTest { + locker.acquireRead("key1") + + var writeAcquired = false + val job = launch { + locker.lockWrite("key2") + writeAcquired = true + locker.unlockWrite("key2") + } + + job.join() + assertTrue(writeAcquired, "Write for different key should succeed") + + locker.releaseRead("key1") + } + + // ==================== Key Write Tests ==================== + + @Test + fun testKeyWriteBlocksReadForSameKey() = runTest { + val key = "testKey" + locker.lockWrite(key) + + var readAcquired = false + val job = launch { + locker.acquireRead(key) + readAcquired = true + locker.releaseRead(key) + } + + delay(200.milliseconds) + assertFalse(readAcquired, "Read for same key should be blocked") + + locker.unlockWrite(key) + job.join() + + assertTrue(readAcquired) + } + + @Test + fun testKeyWriteBlocksGlobalRead() = runTest { + locker.lockWrite("key1") + + var globalReadAcquired = false + val job = launch { + locker.acquireRead() + globalReadAcquired = true locker.releaseRead() - assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits) - - locker.releaseRead(it) } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } - realWithTimeout(1.seconds) { locker.lockWrite() } - assertFails { - realWithTimeout(13.milliseconds) { locker.acquireRead() } - } - assertTrue { locker.unlockWrite() } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + delay(200.milliseconds) + assertFalse(globalReadAcquired, "Global read should be blocked by key write") + + locker.unlockWrite("key1") + job.join() + + assertTrue(globalReadAcquired) } + @Test - fun writesBlockingGlobalWrite() = runTest { - val locker = SmartKeyRWLocker() + fun testKeyWriteIsExclusiveForSameKey() = runTest { + val key = "testKey" + locker.lockWrite(key) - val testKeys = (0 until 100).map { "test$it" } + var secondWriteAcquired = false + val job = launch { + locker.lockWrite(key) + secondWriteAcquired = true + locker.unlockWrite(key) + } - for (i in testKeys.indices) { - val it = testKeys[i] - locker.lockWrite(it) - val previous = testKeys.take(i) - val next = testKeys.drop(i + 1) + delay(200.milliseconds) + assertFalse(secondWriteAcquired, "Second write for same key should be blocked") - previous.forEach { - assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true } + locker.unlockWrite(key) + job.join() + + assertTrue(secondWriteAcquired) + } + + @Test + fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest { + locker.lockWrite("key1") + + val results = mutableMapOf() + + val jobs = listOf( + launch { + locker.acquireRead("key2") + results["read-key2"] = true + locker.releaseRead("key2") + }, + launch { + locker.lockWrite("key3") + results["write-key3"] = true + locker.unlockWrite("key3") } - next.forEach { - assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true } + ) + + jobs.joinAll() + assertEquals(2, results.size, "Operations on different keys should succeed") + + locker.unlockWrite("key1") + } + + // ==================== Complex Scenarios ==================== + + @Test + fun testMultipleReadersThenWriter() = runTest { + val key = "testKey" + val readCount = 5 + val readers = mutableListOf() + + repeat(readCount) { + readers.add(launch { + locker.acquireRead(key) + delay(100.milliseconds) + locker.releaseRead(key) + }) + } + + delay(50.milliseconds) // Let readers acquire + + var writerExecuted = false + val writer = launch { + locker.lockWrite(key) + writerExecuted = true + locker.unlockWrite(key) + } + + delay(50.milliseconds) + assertFalse(writerExecuted, "Writer should wait for all readers") + + readers.joinAll() + writer.join() + + assertTrue(writerExecuted, "Writer should execute after all readers done") + } + + @Test + fun testWriterThenMultipleReaders() = runTest { + val key = "testKey" + + locker.lockWrite(key) + + val readerFlags = mutableListOf() + val readers = List(5) { + launch { + locker.acquireRead(key) + readerFlags.add(true) + locker.releaseRead(key) } } - for (i in testKeys.indices) { - val it = testKeys[i] - assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } } + delay(200.milliseconds) + assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer") - val readPermitsBeforeLock = locker.readSemaphore().freePermits - assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } } - assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits) + locker.unlockWrite(key) + readers.joinAll() - locker.unlockWrite(it) + assertEquals(5, readerFlags.size, "All readers should succeed after writer") + } + + @Test + fun testCascadingLocksWithDifferentKeys() = runTest { + val executed = mutableMapOf() + + launch { + locker.lockWrite("key1") + executed["write-key1-start"] = true + delay(100.milliseconds) + locker.unlockWrite("key1") + executed["write-key1-end"] = true } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } - realWithTimeout(1.seconds) { locker.lockWrite() } - assertFails { - realWithTimeout(13.milliseconds) { locker.acquireRead() } + delay(50.milliseconds) + + launch { + locker.acquireRead("key2") + executed["read-key2"] = true + delay(100.milliseconds) + locker.releaseRead("key2") } - assertTrue { locker.unlockWrite() } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + + delay(200.milliseconds) + + assertTrue(executed["write-key1-start"] == true) + assertTrue(executed["read-key2"] == true) + assertTrue(executed["write-key1-end"] == true) + } + + @Test + fun testReleaseWithoutAcquireReturnsFalse() = runTest { + assertFalse(locker.releaseRead(), "Release without acquire should return false") + assertFalse(locker.releaseRead("key1"), "Release without acquire should return false") + } + + @Test + fun testUnlockWithoutLockReturnsFalse() = runTest { + assertFalse(locker.unlockWrite(), "Unlock without lock should return false") + assertFalse(locker.unlockWrite("key1"), "Unlock without lock should return false") + } + + @Test + fun testProperReleaseReturnsTrue() = runTest { + locker.acquireRead() + assertTrue(locker.releaseRead(), "Release after acquire should return true") + + locker.acquireRead("key1") + assertTrue(locker.releaseRead("key1"), "Release after acquire should return true") + } + + @Test + fun testProperUnlockReturnsTrue() = runTest { + locker.lockWrite() + assertTrue(locker.unlockWrite(), "Unlock after lock should return true") + + locker.lockWrite("key1") + assertTrue(locker.unlockWrite("key1"), "Unlock after lock should return true") + } + + // ==================== Stress Tests ==================== + + @Test + fun stressTestWithMixedOperations() = runTest(timeout = 10.seconds) { + val operations = 100 + val keys = listOf("key1", "key2", "key3", "key4", "key5") + val jobs = mutableListOf() + + repeat(operations) { i -> + val key = keys[i % keys.size] + + when (i % 4) { + 0 -> jobs.add(launch { + locker.acquireRead(key) + delay(10.milliseconds) + locker.releaseRead(key) + }) + 1 -> jobs.add(launch { + locker.lockWrite(key) + delay(10.milliseconds) + locker.unlockWrite(key) + }) + 2 -> jobs.add(launch { + locker.acquireRead() + delay(10.milliseconds) + locker.releaseRead() + }) + 3 -> jobs.add(launch { + locker.lockWrite() + delay(10.milliseconds) + locker.unlockWrite() + }) + } + } + + jobs.joinAll() + // If we reach here without deadlock or exceptions, test passes + } + + @Test + fun testFairnessReadersDontStarveWriters() = runTest(timeout = 5.seconds) { + val key = "testKey" + var writerExecuted = false + + // Start continuous readers + val readers = List(10) { + launch { + repeat(5) { + locker.acquireRead(key) + delay(50.milliseconds) + locker.releaseRead(key) + delay(10.milliseconds) + } + } + } + + delay(100.milliseconds) + + // Try to acquire write lock + val writer = launch { + locker.lockWrite(key) + writerExecuted = true + locker.unlockWrite(key) + } + + readers.joinAll() + writer.join() + + assertTrue(writerExecuted, "Writer should eventually execute") } } diff --git a/gradle.properties b/gradle.properties index ca645dc0da3..ad67b9865ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,8 +8,8 @@ android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g -# https://github.com/google/ksp/issues/2491 -ksp.useKSP2=false +## https://github.com/google/ksp/issues/2491 +#ksp.useKSP2=false # JS NPM @@ -18,5 +18,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.26.6 -android_code_version=305 +version=0.26.7 +android_code_version=306 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41e75db3ba2..cfe1d0ac472 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -kt = "2.2.20" +kt = "2.2.21" kt-serialization = "1.9.0" kt-coroutines = "1.10.2" @@ -8,11 +8,13 @@ kotlinx-browser = "0.5.0" kslog = "1.5.1" -jb-compose = "1.8.2" +jb-compose = "1.9.2" +jb-compose-material3 = "1.9.0" +jb-compose-icons = "1.7.8" jb-exposed = "0.61.0" jb-dokka = "2.1.0" -# 3.50.3.0 contains bug https://github.com/InsanusMokrassar/MicroUtils/actions/runs/18138301958/job/51629588088 +# 3.51.0.0 contains bug, checking with ./gradlew :micro_utils.repos.exposed:jvmTest sqlite = "3.50.1.0" korlibs = "5.4.0" @@ -26,11 +28,11 @@ koin = "4.1.1" okio = "3.16.2" -ksp = "2.2.20-2.0.3" +ksp = "2.3.1" kotlin-poet = "2.2.0" versions = "0.52.0" -nmcp = "1.1.0" +nmcp = "1.2.0" android-gradle = "8.10.+" dexcount = "4.0.0" @@ -89,7 +91,8 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb- jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" } sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } -jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose" } +jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose-material3" } +jb-compose-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "jb-compose-icons" } android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" } android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" } diff --git a/koin/generator/src/main/kotlin/Processor.kt b/koin/generator/src/main/kotlin/Processor.kt index 6933cafb563..a0496fd743a 100644 --- a/koin/generator/src/main/kotlin/Processor.kt +++ b/koin/generator/src/main/kotlin/Processor.kt @@ -27,6 +27,7 @@ import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo import dev.inmo.micro_ksp.generator.safeClassName +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import org.koin.core.Koin @@ -240,9 +241,14 @@ class Processor( ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach { val type = safeClassName { it.type } val targetType = runCatching { - type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type)) + type.parameterizedBy( + *withNoSuchElementWorkaround(emptyArray()) { + it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type + } + ) }.getOrElse { e -> when (e) { + is IllegalArgumentException if (e.message ?.contains("no type argument") == true) -> return@getOrElse type is KSTypeNotPresentException -> e.ksType.toClassName() } if (e is KSTypesNotPresentException) { @@ -251,14 +257,32 @@ class Processor( throw e } }.copy( - nullable = it.nullable + nullable = withNoSuchElementWorkaround(true) { it.nullable } ) - addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory) + addCodeForType( + targetType, + it.name, + withNoSuchElementWorkaround(true) { + it.nullable + }, + withNoSuchElementWorkaround(true) { + it.generateSingle + }, + withNoSuchElementWorkaround(true) { + it.generateFactory + } + ) } ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach { val targetType = TypeVariableName("T", Any::class) - addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory) + addCodeForType( + targetType = targetType, + name = it.name, + nullable = withNoSuchElementWorkaround(true) { it.nullable }, + generateSingle = withNoSuchElementWorkaround(true) { it.generateSingle }, + generateFactory = withNoSuchElementWorkaround(true) { it.generateFactory } + ) } }.build().let { File( diff --git a/ksp/classcasts/generator/src/main/kotlin/Processor.kt b/ksp/classcasts/generator/src/main/kotlin/Processor.kt index bd387d233b8..7c3e8ea6ee7 100644 --- a/ksp/classcasts/generator/src/main/kotlin/Processor.kt +++ b/ksp/classcasts/generator/src/main/kotlin/Processor.kt @@ -10,6 +10,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.symbol.* import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ksp.toClassName +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded @@ -25,7 +26,11 @@ class Processor( ) { val rootAnnotation = ksClassDeclaration.getAnnotationsByType(ClassCastsIncluded::class).first() val (includeRegex: Regex?, excludeRegex: Regex?) = rootAnnotation.let { - it.typesRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) to it.excludeRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) + withNoSuchElementWorkaround("") { + it.typesRegex + }.takeIf { it.isNotEmpty() } ?.let(::Regex) to withNoSuchElementWorkaround("") { + it.excludeRegex + }.takeIf { it.isNotEmpty() } ?.let(::Regex) } val classesSubtypes = mutableMapOf>() @@ -49,7 +54,9 @@ class Processor( when { potentialSubtype === ksClassDeclaration -> {} potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class) -> return@forEach - potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel(rootAnnotation.levelsToInclude.takeIf { it >= 0 }) -> return@forEach + potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel( + withNoSuchElementWorkaround(-1) { rootAnnotation.levelsToInclude }.takeIf { it >= 0 } + ) -> return@forEach excludeRegex ?.matches(simpleName) == true -> return@forEach includeRegex ?.matches(simpleName) == false -> {} else -> classesSubtypes.getOrPut(ksClassDeclaration) { mutableSetOf() }.add(potentialSubtype) @@ -96,7 +103,9 @@ class Processor( @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { (resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix + val prefix = withNoSuchElementWorkaround("") { + it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix + } it.writeFile(prefix = prefix, suffix = "ClassCasts") { FileSpec.builder( it.packageName.asString(), diff --git a/ksp/generator/src/main/kotlin/NoSuchElementWorkaround.kt b/ksp/generator/src/main/kotlin/NoSuchElementWorkaround.kt new file mode 100644 index 00000000000..a5e19134ddc --- /dev/null +++ b/ksp/generator/src/main/kotlin/NoSuchElementWorkaround.kt @@ -0,0 +1,12 @@ +package dev.inmo.micro_ksp.generator + +inline fun withNoSuchElementWorkaround( + default: T, + block: () -> T +): T = runCatching(block).getOrElse { + if (it is NoSuchElementException) { + default + } else { + throw it + } +} diff --git a/ksp/sealed/generator/src/main/kotlin/Processor.kt b/ksp/sealed/generator/src/main/kotlin/Processor.kt index f11bd40257b..f46ae51775e 100644 --- a/ksp/sealed/generator/src/main/kotlin/Processor.kt +++ b/ksp/sealed/generator/src/main/kotlin/Processor.kt @@ -12,6 +12,7 @@ import com.squareup.kotlinpoet.ksp.toClassName import dev.inmo.micro_ksp.generator.buildSubFileName import dev.inmo.micro_ksp.generator.companion import dev.inmo.micro_ksp.generator.findSubClasses +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround @@ -113,7 +114,7 @@ class Processor( val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation val subClasses = ksClassDeclaration.resolveSubclasses( searchIn = resolver.getAllFiles(), - allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false + allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false ).distinct() val subClassesNames = subClasses.filter { it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0 @@ -164,7 +165,15 @@ class Processor( @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { (resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = (it.getGenerateSealedWorkaroundAnnotation) ?.prefix ?.takeIf { + val prefix = runCatching { + (it.getGenerateSealedWorkaroundAnnotation) ?.prefix + }.getOrElse { + if (it is NoSuchElementException) { + "" + } else { + throw it + } + } ?.takeIf { it.isNotEmpty() } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") it.writeFile(prefix = prefix, suffix = "SealedWorkaround") { @@ -184,7 +193,9 @@ class Processor( } } (resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf { + val prefix = withNoSuchElementWorkaround("") { + (it.getGenerateSealedTypesWorkaroundAnnotation)?.prefix + } ?.takeIf { it.isNotEmpty() } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") { diff --git a/ksp/variations/generator/src/main/kotlin/Processor.kt b/ksp/variations/generator/src/main/kotlin/Processor.kt index 59d8ad6bc5e..085196f8105 100644 --- a/ksp/variations/generator/src/main/kotlin/Processor.kt +++ b/ksp/variations/generator/src/main/kotlin/Processor.kt @@ -16,6 +16,7 @@ import com.squareup.kotlinpoet.ksp.toTypeName import dev.inmo.micro_ksp.generator.convertToClassName import dev.inmo.micro_ksp.generator.convertToClassNames import dev.inmo.micro_ksp.generator.findSubClasses +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_utils.ksp.variations.GenerateVariations import dev.inmo.micro_utils.ksp.variations.GenerationVariant @@ -218,7 +219,9 @@ class Processor( @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { (resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix ?.takeIf { + val prefix = withNoSuchElementWorkaround("") { + (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix + } ?.takeIf { it.isNotEmpty() } ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "") it.writeFile(prefix = prefix, suffix = "GeneratedVariation") {