diff --git a/.github/workflows/release-kotlin-preview.yaml b/.github/workflows/release-kotlin-preview.yaml index 99cf4cba..c396a583 100644 --- a/.github/workflows/release-kotlin-preview.yaml +++ b/.github/workflows/release-kotlin-preview.yaml @@ -3,11 +3,11 @@ on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+-Beta' - - 'v[0-9]+.[0-9]+.[0-9]+-new-mm-kotlin-[0-9]+.[0-9]+.[0-9]+-Beta' + - 'v[0-9]+.[0-9]+.[0-9]+-ALPHA-[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+-Beta' - 'v[0-9]+.[0-9]+.[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+-RC' - - 'v[0-9]+.[0-9]+.[0-9]+-new-mm-kotlin-[0-9]+.[0-9]+.[0-9]+-RC' + - 'v[0-9]+.[0-9]+.[0-9]+-ALPHA-[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+-RC' - 'v[0-9]+.[0-9]+.[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+-RC[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+-new-mm-kotlin-[0-9]+.[0-9]+.[0-9]+-RC[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-ALPHA-[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+-RC[0-9]+' jobs: publish-kotlin-libraries: runs-on: macos-12 @@ -32,4 +32,4 @@ jobs: SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} \ No newline at end of file + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} diff --git a/.github/workflows/release-kotlin.yaml b/.github/workflows/release-kotlin.yaml index 9ae9879d..1bf74a72 100644 --- a/.github/workflows/release-kotlin.yaml +++ b/.github/workflows/release-kotlin.yaml @@ -3,9 +3,9 @@ on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-ALPHA-[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+-new-mm' - - 'v[0-9]+.[0-9]+.[0-9]+-new-mm-[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-ALPHA-[0-9]+-kotlin-[0-9]+.[0-9]+.[0-9]+' jobs: publish-kotlin-libraries: runs-on: macos-12 @@ -38,4 +38,4 @@ jobs: SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} - GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} \ No newline at end of file + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} diff --git a/.github/workflows/release-swift.yaml b/.github/workflows/release-swift.yaml index 3a79e601..20446fac 100644 --- a/.github/workflows/release-swift.yaml +++ b/.github/workflows/release-swift.yaml @@ -3,6 +3,7 @@ on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-ALPHA-[0-9]+' jobs: publish-cocoapods-core: runs-on: macos-12 @@ -31,6 +32,18 @@ jobs: run: pod trunk push KMPNativeCoroutinesAsync.podspec --synchronous env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + publish-spm-async: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Publish Async SPM tag + uses: ./.github/actions/publish-spm-tag + with: + package-name: async + version-name: ${{ github.ref_name }} publish-cocoapods-combine: needs: publish-cocoapods-core runs-on: macos-12 @@ -45,6 +58,18 @@ jobs: run: pod trunk push KMPNativeCoroutinesCombine.podspec --synchronous env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + publish-spm-combine: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Publish Combine SPM tag + uses: ./.github/actions/publish-spm-tag + with: + package-name: combine + version-name: ${{ github.ref_name }} publish-cocoapods-rxswift: needs: publish-cocoapods-core runs-on: macos-12 @@ -59,3 +84,15 @@ jobs: run: pod trunk push KMPNativeCoroutinesRxSwift.podspec --synchronous env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + publish-spm-rxswift: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Publish RxSwift SPM tag + uses: ./.github/actions/publish-spm-tag + with: + package-name: rxswift + version-name: ${{ github.ref_name }} diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index e740077d..07c33dcc 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -22,7 +22,7 @@ jobs: os: [ macos-12 ] xcode: [ 13.4.1 ] java: [ 11 ] - module: [ core ] + module: [ core, ksp ] name: ${{ format('{0} ({1}, Xcode {2}, JDK {3})', matrix.module, matrix.os, matrix.xcode, matrix.java) }} runs-on: ${{ matrix.os }} steps: @@ -46,7 +46,7 @@ jobs: - name: Run tests env: GRADLE_MODULE: ${{ format(':kmp-nativecoroutines-{0}', matrix.module) }} - run: ./gradlew $GRADLE_MODULE:allTests + run: ./gradlew $GRADLE_MODULE:check run-swift-tests: if: github.event_name != 'pull_request' || github.event.pull_request.draft == false strategy: diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 2b8a50fc..69e86158 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/runConfigurations/Core_Tests.xml b/.idea/runConfigurations/Core_Tests.xml new file mode 100644 index 00000000..8b9d93fe --- /dev/null +++ b/.idea/runConfigurations/Core_Tests.xml @@ -0,0 +1,23 @@ + + + + + + + false + true + false + + + \ No newline at end of file diff --git a/.idea/runConfigurations/KSP_Tests.xml b/.idea/runConfigurations/KSP_Tests.xml new file mode 100644 index 00000000..0efe1702 --- /dev/null +++ b/.idea/runConfigurations/KSP_Tests.xml @@ -0,0 +1,23 @@ + + + + + + + false + true + false + + + \ No newline at end of file diff --git a/KMPNativeCoroutines.xcodeproj/project.pbxproj b/KMPNativeCoroutines.xcodeproj/project.pbxproj index 16ec6de7..19deadae 100644 --- a/KMPNativeCoroutines.xcodeproj/project.pbxproj +++ b/KMPNativeCoroutines.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 1D7175C1266FF04200846F75 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2EC86A266D290000B118B0 /* Publisher.swift */; }; 1D75A367272D4B3800CFF795 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 1D75A366272D4B3800CFF795 /* RxSwift */; }; 1D75A369272D4B4900CFF795 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 1D75A368272D4B4900CFF795 /* RxSwift */; }; - 1D75A36A272D4BC200CFF795 /* KMPNativeCoroutinesCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D717583266FE90400846F75 /* KMPNativeCoroutinesCore.framework */; }; 1DA5DCEF267613BB002448E3 /* KMPNativeCoroutinesAsync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DA5DCE5267613BB002448E3 /* KMPNativeCoroutinesAsync.framework */; platformFilter = ios; }; 1DA5DCF4267613BB002448E3 /* AsyncFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA5DCF3267613BB002448E3 /* AsyncFunctionTests.swift */; }; 1DA5DCFD26761404002448E3 /* AsyncFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA5DCFC26761404002448E3 /* AsyncFunction.swift */; }; @@ -32,8 +31,8 @@ 1DF3F9E1269369AD004C21F2 /* SingleObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF3F95726935E2A004C21F2 /* SingleObservable.swift */; }; 1DF3F9E2269369B5004C21F2 /* ObservableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF3F94E2693590D004C21F2 /* ObservableTests.swift */; }; 1DF3F9E3269369BA004C21F2 /* SingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF3F959269362D0004C21F2 /* SingleTests.swift */; }; - 1DFB127A26A09796001BE321 /* AsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFB127926A09796001BE321 /* AsyncStream.swift */; }; - 1DFB127C26A0A446001BE321 /* AsyncStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFB127B26A0A446001BE321 /* AsyncStreamTests.swift */; }; + 1DFB127A26A09796001BE321 /* AsyncSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFB127926A09796001BE321 /* AsyncSequence.swift */; }; + 1DFB127C26A0A446001BE321 /* AsyncSequenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFB127B26A0A446001BE321 /* AsyncSequenceTests.swift */; }; 1DFC35FC268A2DE3008393FA /* AsyncResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFC35FB268A2DE3008393FA /* AsyncResult.swift */; }; 1DFC35FE268A2FFE008393FA /* AsyncResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFC35FD268A2FFE008393FA /* AsyncResultTests.swift */; }; /* End PBXBuildFile section */ @@ -46,13 +45,6 @@ remoteGlobalIDString = 1D717582266FE90400846F75; remoteInfo = KMPNativeCoroutinesCore; }; - 1DA5DCF0267613BB002448E3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 1DF8833F266D1BC10058B6F9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 1DA5DCE4267613BB002448E3; - remoteInfo = KMPNativeCoroutinesAsync; - }; 1D75A36C272D4BC200CFF795 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1DF8833F266D1BC10058B6F9 /* Project object */; @@ -60,19 +52,19 @@ remoteGlobalIDString = 1D717582266FE90400846F75; remoteInfo = KMPNativeCoroutinesCore; }; - 1DCA41AB26753C640036C1A3 /* PBXContainerItemProxy */ = { + 1DA5DCF0267613BB002448E3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1DF8833F266D1BC10058B6F9 /* Project object */; proxyType = 1; - remoteGlobalIDString = 1D71759E266FE9B700846F75; - remoteInfo = KMPNativeCoroutinesCombine; + remoteGlobalIDString = 1DA5DCE4267613BB002448E3; + remoteInfo = KMPNativeCoroutinesAsync; }; - 1DF053072698D5E100A16325 /* PBXContainerItemProxy */ = { + 1DCA41AB26753C640036C1A3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1DF8833F266D1BC10058B6F9 /* Project object */; proxyType = 1; - remoteGlobalIDString = 1D717582266FE90400846F75; - remoteInfo = KMPNativeCoroutinesCore; + remoteGlobalIDString = 1D71759E266FE9B700846F75; + remoteInfo = KMPNativeCoroutinesCombine; }; 1DF3F9D226936935004C21F2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -107,8 +99,8 @@ 1DF3F959269362D0004C21F2 /* SingleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleTests.swift; sourceTree = ""; }; 1DF3F9C826936935004C21F2 /* KMPNativeCoroutinesRxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KMPNativeCoroutinesRxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1DF3F9D026936935004C21F2 /* KMPNativeCoroutinesRxSwiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KMPNativeCoroutinesRxSwiftTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 1DFB127926A09796001BE321 /* AsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStream.swift; sourceTree = ""; }; - 1DFB127B26A0A446001BE321 /* AsyncStreamTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStreamTests.swift; sourceTree = ""; }; + 1DFB127926A09796001BE321 /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; + 1DFB127B26A0A446001BE321 /* AsyncSequenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequenceTests.swift; sourceTree = ""; }; 1DFC35FB268A2DE3008393FA /* AsyncResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncResult.swift; sourceTree = ""; }; 1DFC35FD268A2FFE008393FA /* AsyncResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncResultTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -207,7 +199,7 @@ children = ( 1DA5DCFC26761404002448E3 /* AsyncFunction.swift */, 1DFC35FB268A2DE3008393FA /* AsyncResult.swift */, - 1DFB127926A09796001BE321 /* AsyncStream.swift */, + 1DFB127926A09796001BE321 /* AsyncSequence.swift */, ); path = KMPNativeCoroutinesAsync; sourceTree = ""; @@ -217,7 +209,7 @@ children = ( 1DA5DCF3267613BB002448E3 /* AsyncFunctionTests.swift */, 1DFC35FD268A2FFE008393FA /* AsyncResultTests.swift */, - 1DFB127B26A0A446001BE321 /* AsyncStreamTests.swift */, + 1DFB127B26A0A446001BE321 /* AsyncSequenceTests.swift */, ); path = KMPNativeCoroutinesAsyncTests; sourceTree = ""; @@ -357,7 +349,6 @@ buildRules = ( ); dependencies = ( - 1DA5DD022676192F002448E3 /* PBXTargetDependency */, ); name = KMPNativeCoroutinesAsync; productName = KMPNativeCoroutinesAsync; @@ -404,7 +395,6 @@ isa = PBXNativeTarget; buildConfigurationList = 1DF3F9D926936935004C21F2 /* Build configuration list for PBXNativeTarget "KMPNativeCoroutinesRxSwift" */; buildPhases = ( - 1DF3F9C326936935004C21F2 /* Headers */, 1DF3F9C426936935004C21F2 /* Sources */, 1DF3F9C526936935004C21F2 /* Frameworks */, 1DF3F9C626936935004C21F2 /* Resources */, @@ -583,7 +573,7 @@ files = ( 1DA5DCFD26761404002448E3 /* AsyncFunction.swift in Sources */, 1DFC35FC268A2DE3008393FA /* AsyncResult.swift in Sources */, - 1DFB127A26A09796001BE321 /* AsyncStream.swift in Sources */, + 1DFB127A26A09796001BE321 /* AsyncSequence.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -592,7 +582,7 @@ buildActionMask = 2147483647; files = ( 1DA5DCF4267613BB002448E3 /* AsyncFunctionTests.swift in Sources */, - 1DFB127C26A0A446001BE321 /* AsyncStreamTests.swift in Sources */, + 1DFB127C26A0A446001BE321 /* AsyncSequenceTests.swift in Sources */, 1DFC35FE268A2FFE008393FA /* AsyncResultTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -633,27 +623,22 @@ target = 1D717582266FE90400846F75 /* KMPNativeCoroutinesCore */; targetProxy = 1D7175B9266FE9F800846F75 /* PBXContainerItemProxy */; }; + 1D75A36D272D4BC200CFF795 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1D717582266FE90400846F75 /* KMPNativeCoroutinesCore */; + targetProxy = 1D75A36C272D4BC200CFF795 /* PBXContainerItemProxy */; + }; 1DA5DCF1267613BB002448E3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; target = 1DA5DCE4267613BB002448E3 /* KMPNativeCoroutinesAsync */; targetProxy = 1DA5DCF0267613BB002448E3 /* PBXContainerItemProxy */; }; - 1D75A36D272D4BC200CFF795 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 1D717582266FE90400846F75 /* KMPNativeCoroutinesCore */; - targetProxy = 1D75A36C272D4BC200CFF795 /* PBXContainerItemProxy */; - }; 1DCA41AC26753C640036C1A3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 1D71759E266FE9B700846F75 /* KMPNativeCoroutinesCombine */; targetProxy = 1DCA41AB26753C640036C1A3 /* PBXContainerItemProxy */; }; - 1DF053082698D5E100A16325 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 1D717582266FE90400846F75 /* KMPNativeCoroutinesCore */; - targetProxy = 1DF053072698D5E100A16325 /* PBXContainerItemProxy */; - }; 1DF3F9D326936935004C21F2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 1DF3F9C726936935004C21F2 /* KMPNativeCoroutinesRxSwift */; diff --git a/KMPNativeCoroutinesAsync.podspec b/KMPNativeCoroutinesAsync.podspec index 9a3e90bc..e6aaa1d8 100644 --- a/KMPNativeCoroutinesAsync.podspec +++ b/KMPNativeCoroutinesAsync.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'KMPNativeCoroutinesAsync' - s.version = '0.13.3' + s.version = '1.0.0-ALPHA-5' s.summary = 'Swift library for Kotlin Coroutines with Swift Async/Await' s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines' diff --git a/KMPNativeCoroutinesAsync/AsyncFunction.swift b/KMPNativeCoroutinesAsync/AsyncFunction.swift index 36aa300e..9a750936 100644 --- a/KMPNativeCoroutinesAsync/AsyncFunction.swift +++ b/KMPNativeCoroutinesAsync/AsyncFunction.swift @@ -5,65 +5,83 @@ // Created by Rick Clephas on 13/06/2021. // +import Dispatch import KMPNativeCoroutinesCore /// Wraps the `NativeSuspend` in an async function. /// - Parameter nativeSuspend: The native suspend function to await. /// - Returns: The result from the `nativeSuspend`. /// - Throws: Errors thrown by the `nativeSuspend`. -public func asyncFunction(for nativeSuspend: @escaping NativeSuspend) async throws -> Result { - let asyncFunctionActor = AsyncFunctionActor() - return try await withTaskCancellationHandler { - Task { await asyncFunctionActor.cancel() } - } operation: { - try await withUnsafeThrowingContinuation { continuation in - Task { - await asyncFunctionActor.setContinuation(continuation) - let nativeCancellable = nativeSuspend({ output, unit in - Task { await asyncFunctionActor.continueWith(result: output) } - return unit - }, { error, unit in - Task { await asyncFunctionActor.continueWith(error: error) } - return unit - }) - await asyncFunctionActor.setNativeCancellable(nativeCancellable) - } - } - } +public func asyncFunction( + for nativeSuspend: @escaping NativeSuspend +) async throws -> Result { + try await AsyncFunctionTask(nativeSuspend: nativeSuspend).awaitResult() } -internal actor AsyncFunctionActor { - - private var isCancelled = false - private var nativeCancellable: NativeCancellable? = nil - - func setNativeCancellable(_ nativeCancellable: @escaping NativeCancellable) { - guard !isCancelled else { - _ = nativeCancellable() - return - } - self.nativeCancellable = nativeCancellable - } - - func cancel() { - isCancelled = true - _ = nativeCancellable?() - nativeCancellable = nil - } +private class AsyncFunctionTask: @unchecked Sendable { + private let semaphore = DispatchSemaphore(value: 1) + private var nativeCancellable: NativeCancellable? + private var result: Result? = nil + private var error: Failure? = nil + private var cancellationError: Failure? = nil private var continuation: UnsafeContinuation? = nil - func setContinuation(_ continuation: UnsafeContinuation) { - self.continuation = continuation - } - - func continueWith(result: Result) { - continuation?.resume(returning: result) - continuation = nil + init(nativeSuspend: NativeSuspend) { + nativeCancellable = nativeSuspend({ result, unit in + self.semaphore.wait() + defer { self.semaphore.signal() } + self.result = result + if let continuation = self.continuation { + continuation.resume(returning: result) + self.continuation = nil + } + self.nativeCancellable = nil + return unit + }, { error, unit in + self.semaphore.wait() + defer { self.semaphore.signal() } + self.error = error + if let continuation = self.continuation { + continuation.resume(throwing: error) + self.continuation = nil + } + self.nativeCancellable = nil + return unit + }, { cancellationError, unit in + self.semaphore.wait() + defer { self.semaphore.signal() } + self.cancellationError = cancellationError + if let continuation = self.continuation { + continuation.resume(throwing: CancellationError()) + self.continuation = nil + } + self.nativeCancellable = nil + return unit + }) } - func continueWith(error: Error) { - continuation?.resume(throwing: error) - continuation = nil + func awaitResult() async throws -> Result { + try await withTaskCancellationHandler { + _ = nativeCancellable?() + nativeCancellable = nil + } operation: { + try await withUnsafeThrowingContinuation { continuation in + self.semaphore.wait() + defer { self.semaphore.signal() } + if let result = self.result { + continuation.resume(returning: result) + } else if let error = self.error { + continuation.resume(throwing: error) + } else if self.cancellationError != nil { + continuation.resume(throwing: CancellationError()) + } else { + guard self.continuation == nil else { + fatalError("Concurrent calls to awaitResult aren't supported") + } + self.continuation = continuation + } + } + } } } diff --git a/KMPNativeCoroutinesAsync/AsyncResult.swift b/KMPNativeCoroutinesAsync/AsyncResult.swift index 603a8f56..54a2d885 100644 --- a/KMPNativeCoroutinesAsync/AsyncResult.swift +++ b/KMPNativeCoroutinesAsync/AsyncResult.swift @@ -10,7 +10,9 @@ import KMPNativeCoroutinesCore /// Awaits the `NativeSuspend` and returns the result. /// - Parameter nativeSuspend: The native suspend function to await. /// - Returns: The `Result` from the `nativeSuspend`. -public func asyncResult(for nativeSuspend: @escaping NativeSuspend) async -> Result { +public func asyncResult( + for nativeSuspend: @escaping NativeSuspend +) async -> Result { do { return .success(try await asyncFunction(for: nativeSuspend)) } catch { diff --git a/KMPNativeCoroutinesAsync/AsyncSequence.swift b/KMPNativeCoroutinesAsync/AsyncSequence.swift new file mode 100644 index 00000000..9eaa43a8 --- /dev/null +++ b/KMPNativeCoroutinesAsync/AsyncSequence.swift @@ -0,0 +1,108 @@ +// +// AsyncSequence.swift +// AsyncSequence +// +// Created by Rick Clephas on 06/03/2022. +// + +import Dispatch +import KMPNativeCoroutinesCore + +/// Wraps the `NativeFlow` in a `NativeFlowAsyncSequence`. +/// - Parameter nativeFlow: The native flow to collect. +/// - Returns: A `NativeFlowAsyncSequence` that yields the collected values. +public func asyncSequence( + for nativeFlow: @escaping NativeFlow +) -> NativeFlowAsyncSequence { + return NativeFlowAsyncSequence(nativeFlow: nativeFlow) +} + +public struct NativeFlowAsyncSequence: AsyncSequence { + public typealias Element = Output + + var nativeFlow: NativeFlow + + public class Iterator: AsyncIteratorProtocol, @unchecked Sendable { + + private let semaphore = DispatchSemaphore(value: 1) + private var nativeCancellable: NativeCancellable? + private var item: (Output, () -> Unit)? = nil + private var result: Failure?? = Optional.none + private var cancellationError: Failure? = nil + private var continuation: UnsafeContinuation? = nil + + init(nativeFlow: NativeFlow) { + nativeCancellable = nativeFlow({ item, next, unit in + self.semaphore.wait() + defer { self.semaphore.signal() } + if let continuation = self.continuation { + continuation.resume(returning: item) + self.continuation = nil + return next() + } else { + self.item = (item, next) + return unit + } + }, { error, unit in + self.semaphore.wait() + defer { self.semaphore.signal() } + self.result = Optional.some(error) + if let continuation = self.continuation { + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: nil) + } + self.continuation = nil + } + self.nativeCancellable = nil + return unit + }, { cancellationError, unit in + self.semaphore.wait() + defer { self.semaphore.signal() } + self.cancellationError = cancellationError + if let continuation = self.continuation { + continuation.resume(returning: nil) + self.continuation = nil + } + self.nativeCancellable = nil + return unit + }) + } + + public func next() async throws -> Output? { + return try await withTaskCancellationHandler { + _ = nativeCancellable?() + nativeCancellable = nil + } operation: { + try await withUnsafeThrowingContinuation { continuation in + self.semaphore.wait() + defer { self.semaphore.signal() } + if let (item, next) = self.item { + continuation.resume(returning: item) + _ = next() + self.item = nil + } else if let result = self.result { + if let error = result { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: nil) + } + } else if self.cancellationError != nil { + continuation.resume(throwing: CancellationError()) + } else { + guard self.continuation == nil else { + fatalError("Concurrent calls to next aren't supported") + } + self.continuation = continuation + } + } + } + } + } + + public func makeAsyncIterator() -> Iterator { + return Iterator(nativeFlow: nativeFlow) + } +} + diff --git a/KMPNativeCoroutinesAsync/AsyncStream.swift b/KMPNativeCoroutinesAsync/AsyncStream.swift deleted file mode 100644 index e89a892a..00000000 --- a/KMPNativeCoroutinesAsync/AsyncStream.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// AsyncStream.swift -// AsyncStream -// -// Created by Rick Clephas on 15/07/2021. -// - -import KMPNativeCoroutinesCore - -/// Wraps the `NativeFlow` in an `AsyncThrowingStream`. -/// - Parameter nativeFlow: The native flow to collect. -/// - Returns: An stream that yields the collected values. -public func asyncStream(for nativeFlow: @escaping NativeFlow) -> AsyncThrowingStream { - let asyncStreamActor = AsyncStreamActor() - return AsyncThrowingStream { continuation in - continuation.onTermination = { @Sendable _ in - Task { await asyncStreamActor.cancel() } - } - Task { - let nativeCancellable = nativeFlow({ item, unit in - continuation.yield(item) - return unit - }, { error, unit in - if let error = error { - continuation.finish(throwing: error) - } else { - continuation.finish(throwing: nil) - } - return unit - }) - await asyncStreamActor.setNativeCancellable(nativeCancellable) - } - } -} - -internal actor AsyncStreamActor { - - private var isCancelled = false - private var nativeCancellable: NativeCancellable? = nil - - func setNativeCancellable(_ nativeCancellable: @escaping NativeCancellable) { - guard !isCancelled else { - _ = nativeCancellable() - return - } - self.nativeCancellable = nativeCancellable - } - - func cancel() { - isCancelled = true - _ = nativeCancellable?() - nativeCancellable = nil - } -} diff --git a/KMPNativeCoroutinesAsyncTests/AsyncFunctionTests.swift b/KMPNativeCoroutinesAsyncTests/AsyncFunctionTests.swift index 045e6b8d..8417be6b 100644 --- a/KMPNativeCoroutinesAsyncTests/AsyncFunctionTests.swift +++ b/KMPNativeCoroutinesAsyncTests/AsyncFunctionTests.swift @@ -15,10 +15,10 @@ class AsyncFunctionTests: XCTestCase { func testCancellableInvoked() async { var cancelCount = 0 - let nativeSuspend: NativeSuspend = { _, errorCallback in + let nativeSuspend: NativeSuspend = { _, _, cancelCallback in return { cancelCount += 1 - errorCallback(CancellationError(), ()) + cancelCallback(NSError(domain: "Ignored", code: 0), ()) } } let handle = Task { @@ -28,15 +28,16 @@ class AsyncFunctionTests: XCTestCase { handle.cancel() let result = await handle.result XCTAssertEqual(cancelCount, 1, "Cancellable should be invoked once") - guard case .failure(_) = result else { + guard case let .failure(error) = result else { XCTFail("Function should fail with an error") return } + XCTAssertTrue(error is CancellationError, "Error should be a CancellationError") } func testCompletionWithValue() async { let value = TestValue() - let nativeSuspend: NativeSuspend = { resultCallback, _ in + let nativeSuspend: NativeSuspend = { resultCallback, _, _ in resultCallback(value, ()) return { } } @@ -50,7 +51,7 @@ class AsyncFunctionTests: XCTestCase { func testCompletionWithError() async { let sendError = NSError(domain: "Test", code: 0) - let nativeSuspend: NativeSuspend = { _, errorCallback in + let nativeSuspend: NativeSuspend = { _, errorCallback, _ in errorCallback(sendError, ()) return { } } diff --git a/KMPNativeCoroutinesAsyncTests/AsyncResultTests.swift b/KMPNativeCoroutinesAsyncTests/AsyncResultTests.swift index be9d272e..6c071c09 100644 --- a/KMPNativeCoroutinesAsyncTests/AsyncResultTests.swift +++ b/KMPNativeCoroutinesAsyncTests/AsyncResultTests.swift @@ -15,10 +15,10 @@ class AsyncResultTests: XCTestCase { func testCancellableInvoked() async { var cancelCount = 0 - let nativeSuspend: NativeSuspend = { _, errorCallback in + let nativeSuspend: NativeSuspend = { _, _, cancelCallback in return { cancelCount += 1 - errorCallback(CancellationError(), ()) + cancelCallback(NSError(domain: "Ignored", code: 0), ()) } } let handle = Task { @@ -32,15 +32,16 @@ class AsyncResultTests: XCTestCase { XCTFail("Task should complete without an error") return } - guard case .failure(_) = result else { + guard case let .failure(error) = result else { XCTFail("Function should fail with an error") return } + XCTAssertTrue(error is CancellationError, "Error should be a CancellationError") } func testCompletionWithValue() async { let value = TestValue() - let nativeSuspend: NativeSuspend = { resultCallback, _ in + let nativeSuspend: NativeSuspend = { resultCallback, _, _ in resultCallback(value, ()) return { } } @@ -54,7 +55,7 @@ class AsyncResultTests: XCTestCase { func testCompletionWithError() async { let sendError = NSError(domain: "Test", code: 0) - let nativeSuspend: NativeSuspend = { _, errorCallback in + let nativeSuspend: NativeSuspend = { _, errorCallback, _ in errorCallback(sendError, ()) return { } } diff --git a/KMPNativeCoroutinesAsyncTests/AsyncStreamTests.swift b/KMPNativeCoroutinesAsyncTests/AsyncSequenceTests.swift similarity index 53% rename from KMPNativeCoroutinesAsyncTests/AsyncStreamTests.swift rename to KMPNativeCoroutinesAsyncTests/AsyncSequenceTests.swift index 9ee97bab..320d3f43 100644 --- a/KMPNativeCoroutinesAsyncTests/AsyncStreamTests.swift +++ b/KMPNativeCoroutinesAsyncTests/AsyncSequenceTests.swift @@ -1,69 +1,79 @@ // -// AsyncStreamTests.swift -// AsyncStreamTests +// AsyncSequenceTests.swift +// AsyncSequenceTests // -// Created by Rick Clephas on 15/07/2021. +// Created by Rick Clephas on 06/03/2022. // import XCTest import KMPNativeCoroutinesCore import KMPNativeCoroutinesAsync -class AsyncStreamTests: XCTestCase { +class AsyncSequenceTests: XCTestCase { private class TestValue { } func testCancellableInvoked() async { var cancelCount = 0 - let nativeFlow: NativeFlow = { _, _ in - return { cancelCount += 1 } + let nativeFlow: NativeFlow = { _, _, cancelCallback in + return { + cancelCount += 1 + cancelCallback(NSError(domain: "Ignored", code: 0), ()) + } } let handle = Task { - for try await _ in asyncStream(for: nativeFlow) { } + for try await _ in asyncSequence(for: nativeFlow) { } } XCTAssertEqual(cancelCount, 0, "Cancellable shouldn't be invoked yet") handle.cancel() let result = await handle.result XCTAssertEqual(cancelCount, 1, "Cancellable should be invoked once") - guard case .success(_) = result else { - XCTFail("Task should complete without an error") + guard case let .failure(error) = result else { + XCTFail("Task should fail with an error") return } + XCTAssertTrue(error is CancellationError, "Error should be a CancellationError") } func testCompletionWithCorrectValues() async { let values = [TestValue(), TestValue(), TestValue(), TestValue(), TestValue()] - let nativeFlow: NativeFlow = { itemCallback, completionCallback in - for value in values { - itemCallback(value, ()) + let nativeFlow: NativeFlow = { itemCallback, completionCallback, _ in + let handle = Task { + for value in values { + await withCheckedContinuation { continuation in + itemCallback(value, { + continuation.resume() + }, ()) + } + } + completionCallback(nil, ()) } - completionCallback(nil, ()) - return { } + return { handle.cancel() } } var valueCount = 0 do { - for try await receivedValue in asyncStream(for: nativeFlow) { + for try await receivedValue in asyncSequence(for: nativeFlow) { XCTAssertIdentical(receivedValue, values[valueCount], "Received incorrect value") valueCount += 1 } } catch { - XCTFail("Stream should complete without error") + XCTFail("Sequence should complete without error") } XCTAssertEqual(valueCount, values.count, "All values should be received") } func testCompletionWithError() async { let sendError = NSError(domain: "Test", code: 0) - let nativeFlow: NativeFlow = { _, completionCallback in + let nativeFlow: NativeFlow = { _, completionCallback, _ in completionCallback(sendError, ()) return { } } var valueCount = 0 do { - for try await _ in asyncStream(for: nativeFlow) { + for try await _ in asyncSequence(for: nativeFlow) { valueCount += 1 } - XCTFail("Stream should complete with an error") + XCTFail("Sequence should complete with an error") } catch { XCTAssertEqual(error as NSError, sendError, "Received incorrect error") } diff --git a/KMPNativeCoroutinesCombine.podspec b/KMPNativeCoroutinesCombine.podspec index c250b678..47eef543 100644 --- a/KMPNativeCoroutinesCombine.podspec +++ b/KMPNativeCoroutinesCombine.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'KMPNativeCoroutinesCombine' - s.version = '0.13.3' + s.version = '1.0.0-ALPHA-5' s.summary = 'Swift library for Kotlin Coroutines with Combine' s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines' diff --git a/KMPNativeCoroutinesCombine/Future.swift b/KMPNativeCoroutinesCombine/Future.swift index 5d2d63dc..703430ac 100644 --- a/KMPNativeCoroutinesCombine/Future.swift +++ b/KMPNativeCoroutinesCombine/Future.swift @@ -54,6 +54,9 @@ internal class NativeSuspendSubscription: }, { error, unit in self.subscriber?.receive(completion: .failure(error)) return unit + }, { cancellationError, unit in + self.subscriber?.receive(completion: .failure(cancellationError)) + return unit }) } diff --git a/KMPNativeCoroutinesCombine/Publisher.swift b/KMPNativeCoroutinesCombine/Publisher.swift index 200c2060..2f32fa60 100644 --- a/KMPNativeCoroutinesCombine/Publisher.swift +++ b/KMPNativeCoroutinesCombine/Publisher.swift @@ -45,9 +45,9 @@ internal class NativeFlowSubscription: Sub func request(_ demand: Subscribers.Demand) { guard let nativeFlow = nativeFlow, demand >= 1 else { return } self.nativeFlow = nil - nativeCancellable = nativeFlow({ item, unit in - _ = self.subscriber?.receive(item) - return unit + nativeCancellable = nativeFlow({ item, next, _ in + _ = self.subscriber?.receive(item) // TODO: Correctly handle demands + return next() }, { error, unit in if let error = error { self.subscriber?.receive(completion: .failure(error)) @@ -55,6 +55,9 @@ internal class NativeFlowSubscription: Sub self.subscriber?.receive(completion: .finished) } return unit + }, { cancellationError, unit in + self.subscriber?.receive(completion: .failure(cancellationError)) + return unit }) } diff --git a/KMPNativeCoroutinesCombineTests/FutureTests.swift b/KMPNativeCoroutinesCombineTests/FutureTests.swift index 965af55a..57a843b7 100644 --- a/KMPNativeCoroutinesCombineTests/FutureTests.swift +++ b/KMPNativeCoroutinesCombineTests/FutureTests.swift @@ -15,7 +15,7 @@ class FutureTests: XCTestCase { func testCancellableInvoked() { var cancelCount = 0 - let nativeSuspend: NativeSuspend = { _, _ in + let nativeSuspend: NativeSuspend = { _, _, _ in return { cancelCount += 1 } } let cancellable = createFuture(for: nativeSuspend) @@ -27,7 +27,7 @@ class FutureTests: XCTestCase { func testCompletionWithValue() { let value = TestValue() - let nativeSuspend: NativeSuspend = { resultCallback, _ in + let nativeSuspend: NativeSuspend = { resultCallback, _, _ in resultCallback(value, ()) return { } } @@ -51,7 +51,7 @@ class FutureTests: XCTestCase { func testCompletionWithError() { let error = NSError(domain: "Test", code: 0) - let nativeSuspend: NativeSuspend = { _, errorCallback in + let nativeSuspend: NativeSuspend = { _, errorCallback, _ in errorCallback(error, ()) return { } } diff --git a/KMPNativeCoroutinesCombineTests/PublisherTests.swift b/KMPNativeCoroutinesCombineTests/PublisherTests.swift index 76b32922..1798aed5 100644 --- a/KMPNativeCoroutinesCombineTests/PublisherTests.swift +++ b/KMPNativeCoroutinesCombineTests/PublisherTests.swift @@ -15,7 +15,7 @@ class PublisherTests: XCTestCase { func testCancellableInvoked() { var cancelCount = 0 - let nativeFlow: NativeFlow = { _, _ in + let nativeFlow: NativeFlow = { _, _, _ in return { cancelCount += 1 } } let cancellable = createPublisher(for: nativeFlow) @@ -27,9 +27,9 @@ class PublisherTests: XCTestCase { func testCompletionWithCorrectValues() { let values = [TestValue(), TestValue(), TestValue(), TestValue(), TestValue()] - let nativeFlow: NativeFlow = { itemCallback, completionCallback in + let nativeFlow: NativeFlow = { itemCallback, completionCallback, _ in for value in values { - itemCallback(value, ()) + itemCallback(value, {}, ()) } completionCallback(nil, ()) return { } @@ -54,7 +54,7 @@ class PublisherTests: XCTestCase { func testCompletionWithError() { let error = NSError(domain: "Test", code: 0) - let nativeFlow: NativeFlow = { _, completionCallback in + let nativeFlow: NativeFlow = { _, completionCallback, _ in completionCallback(error, ()) return { } } diff --git a/KMPNativeCoroutinesCore.podspec b/KMPNativeCoroutinesCore.podspec index b3353e11..785bf4e1 100644 --- a/KMPNativeCoroutinesCore.podspec +++ b/KMPNativeCoroutinesCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'KMPNativeCoroutinesCore' - s.version = '0.13.3' + s.version = '1.0.0-ALPHA-5' s.summary = 'Swift library for Kotlin Coroutines' s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines' diff --git a/KMPNativeCoroutinesCore/NativeCallback.swift b/KMPNativeCoroutinesCore/NativeCallback.swift index 3e2b9688..be22cf56 100644 --- a/KMPNativeCoroutinesCore/NativeCallback.swift +++ b/KMPNativeCoroutinesCore/NativeCallback.swift @@ -10,3 +10,9 @@ /// The return value is provided as the second argument. /// This way Swift doesn't known what it is/how to get it. public typealias NativeCallback = (T, Unit) -> Unit + +/// A callback with two arguments. +/// +/// The return value is provided as the third argument. +/// This way Swift doesn't known what it is/how to get it. +public typealias NativeCallback2 = (T1, T2, Unit) -> Unit diff --git a/KMPNativeCoroutinesCore/NativeFlow.swift b/KMPNativeCoroutinesCore/NativeFlow.swift index 65c4571e..0cfc6b05 100644 --- a/KMPNativeCoroutinesCore/NativeFlow.swift +++ b/KMPNativeCoroutinesCore/NativeFlow.swift @@ -7,9 +7,10 @@ /// A function that collects a Kotlin coroutines Flow via callbacks. /// -/// The function takes an `onItem` and `onComplete` callback +/// The function takes an `onItem`, `onComplete` and `onCancelled` callback /// and returns a cancellable that can be used to cancel the collection. public typealias NativeFlow = ( - _ onItem: @escaping NativeCallback, - _ onComplete: @escaping NativeCallback + _ onItem: @escaping NativeCallback2 Unit, Unit>, + _ onComplete: @escaping NativeCallback, + _ onCancelled: @escaping NativeCallback ) -> NativeCancellable diff --git a/KMPNativeCoroutinesCore/NativeSuspend.swift b/KMPNativeCoroutinesCore/NativeSuspend.swift index 3f03bf42..c76a740c 100644 --- a/KMPNativeCoroutinesCore/NativeSuspend.swift +++ b/KMPNativeCoroutinesCore/NativeSuspend.swift @@ -7,9 +7,10 @@ /// A function that awaits a suspend function via callbacks. /// -/// The function takes an `onResult` and `onError` callback +/// The function takes an `onResult`, `onError` and `onCancelled` callback /// and returns a cancellable that can be used to cancel the suspend function. public typealias NativeSuspend = ( _ onResult: @escaping NativeCallback, - _ onError: @escaping NativeCallback + _ onError: @escaping NativeCallback, + _ onCancelled: @escaping NativeCallback ) -> NativeCancellable diff --git a/KMPNativeCoroutinesRxSwift.podspec b/KMPNativeCoroutinesRxSwift.podspec index 759e9035..28f0daa3 100644 --- a/KMPNativeCoroutinesRxSwift.podspec +++ b/KMPNativeCoroutinesRxSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'KMPNativeCoroutinesRxSwift' - s.version = '0.13.3' + s.version = '1.0.0-ALPHA-5' s.summary = 'Swift library for Kotlin Coroutines with RxSwift' s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines' diff --git a/KMPNativeCoroutinesRxSwift/Observable.swift b/KMPNativeCoroutinesRxSwift/Observable.swift index 1534c435..278865a4 100644 --- a/KMPNativeCoroutinesRxSwift/Observable.swift +++ b/KMPNativeCoroutinesRxSwift/Observable.swift @@ -16,9 +16,9 @@ public func createObservable( ) -> Observable { return Observable.deferred { return Observable.create { observer in - let nativeCancellable = nativeFlow({ item, unit in + let nativeCancellable = nativeFlow({ item, next, _ in observer.onNext(item) - return unit + return next() }, { error, unit in if let error = error { observer.onError(error) @@ -26,6 +26,9 @@ public func createObservable( observer.onCompleted() } return unit + }, { cancellationError, unit in + observer.onError(cancellationError) + return unit }) return Disposables.create { _ = nativeCancellable() } } diff --git a/KMPNativeCoroutinesRxSwift/Single.swift b/KMPNativeCoroutinesRxSwift/Single.swift index 83ca6e21..91946441 100644 --- a/KMPNativeCoroutinesRxSwift/Single.swift +++ b/KMPNativeCoroutinesRxSwift/Single.swift @@ -22,6 +22,9 @@ public func createSingle( }, { error, unit in observer(.failure(error)) return unit + }, { cancellationError, unit in + observer(.failure(cancellationError)) + return unit }) return Disposables.create { _ = nativeCancellable() } } diff --git a/KMPNativeCoroutinesRxSwiftTests/ObservableTests.swift b/KMPNativeCoroutinesRxSwiftTests/ObservableTests.swift index eeae0e51..94e06eda 100644 --- a/KMPNativeCoroutinesRxSwiftTests/ObservableTests.swift +++ b/KMPNativeCoroutinesRxSwiftTests/ObservableTests.swift @@ -15,7 +15,7 @@ class ObservableTests: XCTestCase { func testDisposableInvoked() { var cancelCount = 0 - let nativeFlow: NativeFlow = { _, _ in + let nativeFlow: NativeFlow = { _, _, _ in return { cancelCount += 1 } } let disposable = createObservable(for: nativeFlow).subscribe() @@ -26,9 +26,9 @@ class ObservableTests: XCTestCase { func testCompletionWithCorrectValues() { let values = [TestValue(), TestValue(), TestValue(), TestValue(), TestValue()] - let nativeFlow: NativeFlow = { itemCallback, completionCallback in + let nativeFlow: NativeFlow = { itemCallback, completionCallback, _ in for value in values { - itemCallback(value, ()) + itemCallback(value, {}, ()) } completionCallback(nil, ()) return { } @@ -51,7 +51,7 @@ class ObservableTests: XCTestCase { func testCompletionWithError() { let error = NSError(domain: "Test", code: 0) - let nativeFlow: NativeFlow = { _, completionCallback in + let nativeFlow: NativeFlow = { _, completionCallback, _ in completionCallback(error, ()) return { } } diff --git a/KMPNativeCoroutinesRxSwiftTests/SingleTests.swift b/KMPNativeCoroutinesRxSwiftTests/SingleTests.swift index 094767d6..fd3c0081 100644 --- a/KMPNativeCoroutinesRxSwiftTests/SingleTests.swift +++ b/KMPNativeCoroutinesRxSwiftTests/SingleTests.swift @@ -15,7 +15,7 @@ class SingleTests: XCTestCase { func testDisposableInvoked() { var cancelCount = 0 - let nativeSuspend: NativeSuspend = { _, _ in + let nativeSuspend: NativeSuspend = { _, _, _ in return { cancelCount += 1 } } let disposable = createSingle(for: nativeSuspend).subscribe() @@ -26,7 +26,7 @@ class SingleTests: XCTestCase { func testCompletionWithValue() { let value = TestValue() - let nativeSuspend: NativeSuspend = { resultCallback, _ in + let nativeSuspend: NativeSuspend = { resultCallback, _, _ in resultCallback(value, ()) return { } } @@ -44,7 +44,7 @@ class SingleTests: XCTestCase { func testCompletionWithError() { let error = NSError(domain: "Test", code: 0) - let nativeSuspend: NativeSuspend = { _, errorCallback in + let nativeSuspend: NativeSuspend = { _, errorCallback, _ in errorCallback(error, ()) return { } } diff --git a/MIGRATING_TO_V1.md b/MIGRATING_TO_V1.md new file mode 100644 index 00000000..b01d870e --- /dev/null +++ b/MIGRATING_TO_V1.md @@ -0,0 +1,94 @@ +# Migrating to version 1.0 + +The 1.0 release will bring some improvements that require some changes in your code 😅. + +> **Warning**: the 1.0 release requires Kotlin 1.8.0-Beta or above. +> Checkout the [README](README.md) for Kotlin compatibility info. + +> **Note**: make sure to use the same library versions for your Kotlin and Swift code! + +Known issues: +* [#83](https://github.com/rickclephas/KMP-NativeCoroutines/issues/83) +Non-embeddable compiler JAR compilations are broken in v1.0 + +## KSP + +Starting with v1.0 the plugin is using [KSP](https://github.com/google/ksp) to generate the required Kotlin code. +So make sure to add KSP to your project (if you haven't already): +```diff + plugins { ++ id("com.google.devtools.ksp") version "" + id("com.rickclephas.kmp.nativecoroutines") version "" + } +``` + +### Annotate your declarations + +To tell the plugin what declarations should be refined for ObjC/Swift you'll need to annotate them: +```diff ++ @NativeCoroutines + val time: StateFlow + ++ @NativeCoroutines + suspend fun getRandomLetters(): String = "" +``` + +> **Note**: error messages and IDE support are currently limited. +> Please track [#81](https://github.com/rickclephas/KMP-NativeCoroutines/issues/81) and +> [#82](https://github.com/rickclephas/KMP-NativeCoroutines/issues/82) for improved error messages. + +### Custom CoroutineScope + +Custom `CoroutineScope`s are still supported, just make sure they are either `internal` or `public`. + +### Extension properties/functions + +The plugin is now generating extension properties/functions and no longer modifies the original class. +ObjC/Swift interop have a couple of limitations with extension functions. +Take a look at the Kotlin [docs](https://kotlinlang.org/docs/native-objc-interop.html#extensions-and-category-members) +for more information. + +## Improved property/function names + +Property and function names are now being reused for their native versions. +So go ahead and remove all those `Native` suffixes from your Swift code: +```diff +- createPublisher(for: clock.timeNative) ++ createPublisher(for: clock.time) + +- createFuture(for: randomLettersGenerator.getRandomLettersNative()) ++ createFuture(for: randomLettersGenerator.getRandomLetters()) +``` + +The value and replay cache property names also drop the `Native` suffix: +```diff +- let value = clock.timeNativeValue ++ let value = clock.timeValue + +- let replayCache = clock.timeNativeReplayCache ++ let replayCache = clock.timeReplayCache +``` + +> **Note**: you can now customize the value and replay cache suffixes, +> or if desired completely remove those properties from the generated code. +> Checkout the [README](README.md#name-suffix) for more info. + +## AsyncSequence + +The `asyncStream(for:)` function has been renamed to `asyncSequence(for:)` and now returns an `AsyncSequence`. +```diff +- let lettersStream = asyncStream(for: randomLettersGenerator.getRandomLettersFlow()) ++ let lettersStream = asyncSequence(for: randomLettersGenerator.getRandomLettersFlow()) + for try await letters in lettersStream { + print("Got random letters: \(letters)") + } +``` + +Collecting a `Flow` with an `AsyncSequence` will now apply backpressure. +Meaning your Swift code is no longer buffering elements, +but will suspend your Kotlin code in the same way collecting the `Flow` in Kotlin would. + +## Swift CancellationError + +The Swift Concurrency functions will throw a [`CancellationError`](https://developer.apple.com/documentation/swift/cancellationerror) +instead of the `KotlinCancellationException` wrapped `NSError`. diff --git a/README.md b/README.md index a2ee7119..46a04043 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,11 @@ A library to use Kotlin Coroutines from Swift code in KMP apps. -> **Note**: checkout the updated [README](https://github.com/rickclephas/KMP-NativeCoroutines/blob/dev-1.0/README.md) -> and [migration steps](https://github.com/rickclephas/KMP-NativeCoroutines/blob/dev-1.0/MIGRATING_TO_V1.md) -> for the 1.0 pre-release versions. +> Looking to upgrade from the 0.x releases? Checkout the [migration steps](MIGRATING_TO_V1.md). ## Why this library? -Both KMP and Kotlin Coroutines are amazing but together they have some limitations. +Both KMP and Kotlin Coroutines are amazing, but together they have some limitations. The most important limitation is cancellation support. Kotlin suspend functions are exposed to Swift as functions with a completion handler. @@ -26,27 +24,14 @@ This library solves both of these limitations 😄. ## Compatibility -> **Note**: version `0.13` and above only support the [new Kotlin Native memory model][new-mm]. -> Previous versions were using the [`-native-mt`][native-mt] versions of the kotlinx.coroutines library. -> To use the new memory model with older versions you should use the `-new-mm` variant. - -[new-mm]: https://github.com/JetBrains/kotlin/blob/0b871d7534a9c8e90fb9ad61cd5345716448d08c/kotlin-native/NEW_MM.md -[native-mt]: https://github.com/kotlin/kotlinx.coroutines/issues/462 - -The latest version of the library uses Kotlin version `1.7.21`. +The latest version of the library uses Kotlin version `1.8.20`. Compatibility versions for older Kotlin versions are also available: -| Version | Version suffix | Kotlin | Coroutines | -|--------------|-------------------|:----------:|:---------------:| -| _latest_ | -kotlin-1.8.0-RC | 1.8.0-RC | 1.6.4 | -| **_latest_** | **_no suffix_** | **1.7.21** | **1.6.4** | -| 0.13.1 | _no suffix_ | 1.7.20 | 1.6.4 | -| 0.13.0 | _no suffix_ | 1.7.10 | 1.6.4 | -| 0.12.6 | -kotlin-1.7.20-RC | 1.7.20-RC | 1.6.4 | -| 0.12.6 | -new-mm | 1.7.10 | 1.6.3 | -| 0.12.6 | _no suffix_ | 1.7.10 | 1.6.3-native-mt | -| 0.12.5 | -new-mm | 1.7.0 | 1.6.3 | -| 0.12.5 | _no suffix_ | 1.7.0 | 1.6.3-native-mt | +| Version | Version suffix | Kotlin | KSP | Coroutines | +|---------------|-----------------|:----------:|:----------:|:----------:| +| **_latest_** | **_no suffix_** | **1.8.20** | **1.0.10** | **1.6.4** | +| 1.0.0-ALPHA-5 | _no suffix_ | 1.8.10 | 1.0.9 | 1.6.4 | +| 1.0.0-ALPHA-4 | _no suffix_ | 1.8.0 | 1.0.8 | 1.6.4 | You can choose from a couple of Swift implementations. Depending on the implementation you can support as low as iOS 9, macOS 10.9, tvOS 9 and watchOS 3: @@ -72,9 +57,16 @@ Make sure to always use the same versions for all the libraries! For Kotlin just add the plugin to your `build.gradle.kts`: ```kotlin plugins { + id("com.google.devtools.ksp") version "" id("com.rickclephas.kmp.nativecoroutines") version "" } ``` +and make sure to opt in to the experimental `@ObjCName` annotation: +```kotlin +kotlin.sourceSets.all { + languageSettings.optIn("kotlin.experimental.ExperimentalObjCName") +} +``` ### Swift (Swift Package Manager) @@ -92,11 +84,14 @@ Or add it in Xcode by going to `File` > `Add Packages...` and providing the URL: > **Note**: the version for the Swift package should not contain the Kotlin version suffix > (e.g. `-new-mm` or `-kotlin-1.6.0`). +> **Note**: if you only need a single implementation you can also use the SPM specific versions with suffixes +> `-spm-async`, `-spm-combine` and `-spm-rxswift`. + ### Swift (CocoaPods) If you use CocoaPods add one or more of the following libraries to your `Podfile`: ```ruby -pod 'KMPNativeCoroutinesAsync', '' # Swift 5.5 Async/Await implementation +pod 'KMPNativeCoroutinesAsync', '' # Swift Concurrency implementation pod 'KMPNativeCoroutinesCombine', '' # Combine implementation pod 'KMPNativeCoroutinesRxSwift', '' # RxSwift implementation ``` @@ -109,105 +104,110 @@ Just use the wrapper functions in Swift to get async functions, AsyncStreams, Pu ### Kotlin -> **Warning**: the Kotlin part of this library consists of helper functions and a Kotlin compiler plugin. -> Using the plugin removes the boilerplate code from your project, however **Kotlin compiler plugins aren't stable**! -> -> The plugin is known to cause recursion errors in some scenarios such as in [#4][GH-4] and [#23][GH-23]. -> To prevent such recursion errors it's best to explicitly define the (return) types of public -> properties and functions. - -[GH-4]: https://github.com/rickclephas/KMP-NativeCoroutines/issues/4 -[GH-23]: https://github.com/rickclephas/KMP-NativeCoroutines/issues/23 +The plugin will automagically generate the necessary code for you! 🔮 +Just annotate your coroutines declarations with `@NativeCoroutines` (or `@NativeCoroutinesState`). -The plugin will automagically generate the necessary code for you! 🔮 +#### Flows -Your `Flow` properties/functions get a `Native` version: +Your `Flow` properties/functions get a native version: ```kotlin class Clock { // Somewhere in your Kotlin code you define a Flow property + // and annotate it with @NativeCoroutines + @NativeCoroutines val time: StateFlow // This can be any kind of Flow - - // The plugin will generate this native property for you - val timeNative - get() = time.asNativeFlow() } ``` -In case of a `StateFlow` or `SharedFlow` property you also get a `NativeValue` or `NativeReplayCache` property: -```kotlin -// For the StateFlow defined above the plugin will generate this native value property -val timeNativeValue - get() = time.value +
Generated code +

-// In case of a SharedFlow the plugin would generate this native replay cache property -val timeNativeReplayCache - get() = time.replayCache +The plugin will generate this native property for you: +```kotlin +@ObjCName(name = "time") +val Clock.timeNative + get() = time.asNativeFlow() ``` -The plugin also generates `Native` versions for all your suspend functions: +For the `StateFlow` defined above the plugin will also generate this value property: ```kotlin -class RandomLettersGenerator { - // Somewhere in your Kotlin code you define a suspend function - suspend fun getRandomLetters(): String { - // Code to generate some random letters - } - - // The plugin will generate this native function for you - fun getRandomLettersNative() = - nativeSuspend { getRandomLetters() } -} +val Clock.timeValue + get() = time.value ``` -#### Global properties and functions - -The plugin is currently unable to generate native versions for global properties and functions. -In such cases you have to manually create the native versions in your Kotlin native code. - -#### Custom suffix - -If you don't like the naming of these generated properties/functions, you can easily change the suffix. -For example add the following to your `build.gradle.kts` to use the suffix `Apple`: +In case of a `SharedFlow` the plugin would generate a replay cache property: ```kotlin -nativeCoroutines { - suffix = "Apple" -} +val Clock.timeReplayCache + get() = time.replayCache ``` +

+
-#### Custom CoroutineScope +#### StateFlows -For more control you can provide a custom `CoroutineScope` with the `NativeCoroutineScope` annotation: +Using `StateFlow` properties to track state (like in a view model)? +Use the `@NativeCoroutinesState` annotation instead: ```kotlin class Clock { - @NativeCoroutineScope - internal val coroutineScope = CoroutineScope(job + Dispatchers.Default) + // Somewhere in your Kotlin code you define a StateFlow property + // and annotate it with @NativeCoroutinesState + @NativeCoroutinesState + val time: StateFlow // This must be a StateFlow } ``` -If you don't provide a `CoroutineScope` the default scope will be used which is defined as: +
Generated code +

+ +The plugin will generate these native properties for you: ```kotlin -internal val defaultCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) +@ObjCName(name = "time") +val Clock.timeValue + get() = time.value + +val Clock.timeFlow + get() = time.asNativeFlow() ``` +

+
-#### Ignoring declarations +#### Suspend functions -Use the `NativeCoroutinesIgnore` annotation to tell the plugin to ignore a property or function: +The plugin also generates native versions for your annotated suspend functions: ```kotlin -@NativeCoroutinesIgnore -val ignoredFlowProperty: Flow +class RandomLettersGenerator { + // Somewhere in your Kotlin code you define a suspend function + // and annotate it with @NativeCoroutines + @NativeCoroutines + suspend fun getRandomLetters(): String { + // Code to generate some random letters + } +} +``` -@NativeCoroutinesIgnore -suspend fun ignoredSuspendFunction() { } +
Generated code +

+ +The plugin will generate this native function for you: +```kotlin +@ObjCName(name = "getRandomLetters") +fun RandomLettersGenerator.getRandomLettersNative() = + nativeSuspend { getRandomLetters() } ``` +

+
+ +### Swift Concurrency -### Swift 5.5 Async/Await +The Async implementation provides some functions to get async Swift functions and `AsyncSequence`s. -The Async implementation provides some functions to get async Swift functions and `AsyncStream`s. +#### Async functions Use the `asyncFunction(for:)` function to get an async function that can be awaited: ```swift let handle = Task { do { - let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLettersNative()) + let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLetters()) print("Got random letters: \(letters)") } catch { print("Failed with error: \(error)") @@ -220,18 +220,20 @@ handle.cancel() or if you don't like these do-catches you can use the `asyncResult(for:)` function: ```swift -let result = await asyncResult(for: randomLettersGenerator.getRandomLettersNative()) +let result = await asyncResult(for: randomLettersGenerator.getRandomLetters()) if case let .success(letters) = result { print("Got random letters: \(letters)") } ``` -For `Flow`s there is the `asyncStream(for:)` function to get an `AsyncStream`: +#### AsyncSequence + +For `Flow`s there is the `asyncSequence(for:)` function to get an `AsyncSequence`: ```swift let handle = Task { do { - let stream = asyncStream(for: randomLettersGenerator.getRandomLettersFlowNative()) - for try await letters in stream { + let sequence = asyncSequence(for: randomLettersGenerator.getRandomLettersFlow()) + for try await letters in sequence { print("Got random letters: \(letters)") } } catch { @@ -247,10 +249,15 @@ handle.cancel() The Combine implementation provides a couple functions to get an `AnyPublisher` for your Coroutines code. +> **Note**: these functions create deferred `AnyPublisher`s. +> Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function. + +#### Publisher + For your `Flow`s use the `createPublisher(for:)` function: ```swift // Create an AnyPublisher for your flow -let publisher = createPublisher(for: clock.timeNative) +let publisher = createPublisher(for: clock.time) // Now use this publisher as you would any other let cancellable = publisher.sink { completion in @@ -263,10 +270,17 @@ let cancellable = publisher.sink { completion in cancellable.cancel() ``` +You can also use the `createPublisher(for:)` function for suspend functions that return a `Flow`: +```swift +let publisher = createPublisher(for: randomLettersGenerator.getRandomLettersFlow()) +``` + +#### Future + For the suspend functions you should use the `createFuture(for:)` function: ```swift // Create a Future/AnyPublisher for the suspend function -let future = createFuture(for: randomLettersGenerator.getRandomLettersNative()) +let future = createFuture(for: randomLettersGenerator.getRandomLetters()) // Now use this future as you would any other let cancellable = future.sink { completion in @@ -279,22 +293,19 @@ let cancellable = future.sink { completion in cancellable.cancel() ``` -You can also use the `createPublisher(for:)` function for suspend functions that return a `Flow`: -```swift -let publisher = createPublisher(for: randomLettersGenerator.getRandomLettersFlowNative()) -``` - -> **Note**: these functions create deferred `AnyPublisher`s. -> Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function. - ### RxSwift The RxSwift implementation provides a couple functions to get an `Observable` or `Single` for your Coroutines code. +> **Note**: these functions create deferred `Observable`s and `Single`s. +> Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function. + +#### Observable + For your `Flow`s use the `createObservable(for:)` function: ```swift // Create an observable for your flow -let observable = createObservable(for: clock.timeNative) +let observable = createObservable(for: clock.time) // Now use this observable as you would any other let disposable = observable.subscribe(onNext: { value in @@ -311,10 +322,17 @@ let disposable = observable.subscribe(onNext: { value in disposable.dispose() ``` +You can also use the `createObservable(for:)` function for suspend functions that return a `Flow`: +```swift +let observable = createObservable(for: randomLettersGenerator.getRandomLettersFlow()) +``` + +#### Single + For the suspend functions you should use the `createSingle(for:)` function: ```swift // Create a single for the suspend function -let single = createSingle(for: randomLettersGenerator.getRandomLettersNative()) +let single = createSingle(for: randomLettersGenerator.getRandomLetters()) // Now use this single as you would any other let disposable = single.subscribe(onSuccess: { value in @@ -329,10 +347,62 @@ let disposable = single.subscribe(onSuccess: { value in disposable.dispose() ``` -You can also use the `createObservable(for:)` function for suspend functions that return a `Flow`: -```swift -let observable = createObservable(for: randomLettersGenerator.getRandomLettersFlowNative()) +## Customize + +There are a number of ways you can customize the generated Kotlin code. + +### Name suffix + +Don't like the naming of the generated properties/functions? +Specify your own custom suffixes in your `build.gradle.kts` file: +```kotlin +nativeCoroutines { + // The suffix used to generate the native coroutine function and property names. + suffix = "Native" + // The suffix used to generate the native coroutine file names. + // Note: defaults to the suffix value when `null`. + fileSuffix = null + // The suffix used to generate the StateFlow value property names, + // or `null` to remove the value properties. + flowValueSuffix = "Value" + // The suffix used to generate the SharedFlow replayCache property names, + // or `null` to remove the replayCache properties. + flowReplayCacheSuffix = "ReplayCache" + // The suffix used to generate the native state property names. + stateSuffix = "Value" + // The suffix used to generate the `StateFlow` flow property names, + // or `null` to remove the flow properties. + stateFlowSuffix = "Flow" +} ``` -> **Note**: these functions create deferred `Observable`s and `Single`s. -> Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function. +### CoroutineScope + +For more control you can provide a custom `CoroutineScope` with the `NativeCoroutineScope` annotation: +```kotlin +class Clock { + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(job + Dispatchers.Default) +} +``` + +> **Note**: your custom coroutine scope must be either `internal` or `public`. + +If you don't provide a `CoroutineScope` the default scope will be used which is defined as: +```kotlin +internal val defaultCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) +``` + +> **Note**: KMP-NativeCoroutines has built-in support for [KMM-ViewModel](https://github.com/rickclephas/KMM-ViewModel). +> Coroutines inside your `KMMViewModel` will (by default) use the `CoroutineScope` from the `ViewModelScope`. + +### Ignoring declarations + +Use the `NativeCoroutinesIgnore` annotation to tell the plugin to ignore a property or function: +```kotlin +@NativeCoroutinesIgnore +val ignoredFlowProperty: Flow + +@NativeCoroutinesIgnore +suspend fun ignoredSuspendFunction() { } +``` diff --git a/build.gradle.kts b/build.gradle.kts index 6bcb03ba..9e356916 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ buildscript { allprojects { group = "com.rickclephas.kmp" - version = "0.13.3" + version = "1.0.0-ALPHA-5-kotlin-1.8.20-RC2" repositories { mavenCentral() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12bc9191..392a522f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,22 @@ [versions] -kotlin = "1.8.0" +kotlin = "1.8.20" kotlinx-coroutines = "1.6.4" +ksp = "1.8.20-1.0.10" +kotlinpoet = "1.12.0" [libraries] kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin" } kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } +kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } + +# Testing libraries +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlinCompileTesting-ksp = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version = "1.4.9" } # Sample libraries kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.3.1" } @@ -20,3 +28,4 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref # Sample plugins kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutines.kt b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutines.kt new file mode 100644 index 00000000..85be4ce6 --- /dev/null +++ b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutines.kt @@ -0,0 +1,14 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.experimental.ExperimentalObjCRefinement +import kotlin.native.HidesFromObjC + +/** + * Identifies properties and functions that require a native coroutines version. + */ +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.BINARY) +@MustBeDocumented +@OptIn(ExperimentalObjCRefinement::class) +@HidesFromObjC +annotation class NativeCoroutines diff --git a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutinesState.kt b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutinesState.kt new file mode 100644 index 00000000..6707d844 --- /dev/null +++ b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutinesState.kt @@ -0,0 +1,14 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.experimental.ExperimentalObjCRefinement +import kotlin.native.HidesFromObjC + +/** + * Identifies `StateFlow` properties that require a native state version. + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +@MustBeDocumented +@OptIn(ExperimentalObjCRefinement::class) +@HidesFromObjC +annotation class NativeCoroutinesState diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCompilerPluginRegistrar.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCompilerPluginRegistrar.kt index 2731519c..d54ec699 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCompilerPluginRegistrar.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCompilerPluginRegistrar.kt @@ -1,11 +1,8 @@ package com.rickclephas.kmp.nativecoroutines.compiler -import com.rickclephas.kmp.nativecoroutines.compiler.utils.NameGenerator -import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension @OptIn(ExperimentalCompilerApi::class) class KmpNativeCoroutinesCompilerPluginRegistrar: CompilerPluginRegistrar() { @@ -13,10 +10,6 @@ class KmpNativeCoroutinesCompilerPluginRegistrar: CompilerPluginRegistrar() { override val supportsK2: Boolean = false // TODO: Add K2 support override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { - val suffix = configuration.get(SUFFIX_KEY) ?: return - val nameGenerator = NameGenerator(suffix) - SyntheticResolveExtension.registerExtension(KmpNativeCoroutinesSyntheticResolveExtension(nameGenerator)) - SyntheticResolveExtension.registerExtension(KmpNativeCoroutinesSyntheticResolveExtension.RecursiveCallSyntheticResolveExtension()) - IrGenerationExtension.registerExtension(KmpNativeCoroutinesIrGenerationExtension(nameGenerator)) + } } diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt deleted file mode 100644 index 6c922575..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler - -import com.rickclephas.kmp.nativecoroutines.compiler.utils.NameGenerator -import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.ir.declarations.* - -internal class KmpNativeCoroutinesIrGenerationExtension( - private val nameGenerator: NameGenerator -): IrGenerationExtension { - - override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { - moduleFragment.accept(KmpNativeCoroutinesIrTransformer(pluginContext, nameGenerator), null) - } -} - diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt deleted file mode 100644 index 9964d36d..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt +++ /dev/null @@ -1,180 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler - -import com.rickclephas.kmp.nativecoroutines.compiler.utils.* -import com.rickclephas.kmp.nativecoroutines.compiler.utils.isNativeCoroutinesFunction -import com.rickclephas.kmp.nativecoroutines.compiler.utils.referenceNativeSuspendFunction -import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder -import org.jetbrains.kotlin.ir.IrStatement -import org.jetbrains.kotlin.ir.builders.* -import org.jetbrains.kotlin.ir.declarations.IrFunction -import org.jetbrains.kotlin.ir.declarations.IrProperty -import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction -import org.jetbrains.kotlin.ir.expressions.IrBlockBody -import org.jetbrains.kotlin.ir.expressions.IrExpression -import org.jetbrains.kotlin.ir.types.classifierOrFail -import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl -import org.jetbrains.kotlin.ir.types.typeOrNull -import org.jetbrains.kotlin.ir.util.* - -internal class KmpNativeCoroutinesIrTransformer( - private val context: IrPluginContext, - private val nameGenerator: NameGenerator -): IrElementTransformerVoidWithContext() { - - private val nativeSuspendFunction = context.referenceNativeSuspendFunction() - private val nativeFlowFunction = context.referenceNativeFlowFunction() - private val stateFlowValueProperty = context.referenceStateFlowValueProperty() - private val sharedFlowReplayCacheProperty = context.referenceSharedFlowReplayCacheProperty() - private val listClass = context.referenceListClass() - - override fun visitPropertyNew(declaration: IrProperty): IrStatement { - if (declaration.isFakeOverride || declaration.getter?.body != null || declaration.setter != null) - return super.visitPropertyNew(declaration) - - val isNativeName = nameGenerator.isNativeName(declaration.name) - val isNativeValueName = nameGenerator.isNativeValueName(declaration.name) - val isNativeReplayCacheName = nameGenerator.isNativeReplayCacheName(declaration.name) - if (!isNativeName && !isNativeValueName && !isNativeReplayCacheName) - return super.visitPropertyNew(declaration) - - val getter = declaration.getter ?: return super.visitPropertyNew(declaration) - val originalName = nameGenerator.createOriginalName(declaration.name) - val originalProperty = declaration.parentAsClass.properties.single { - it.name == originalName && it.needsNativeProperty - } - val originalGetter = originalProperty.getter - ?: throw IllegalStateException("Original property doesn't have a getter") - - getter.body = when { - isNativeName -> createNativeBody(getter, originalGetter) - isNativeValueName -> createNativeValueBody(getter, originalGetter) - isNativeReplayCacheName -> createNativeReplayCacheBody(getter, originalGetter) - else -> throw IllegalStateException("Unsupported property type") - } - return super.visitPropertyNew(declaration) - } - - private fun createNativeValueBody(getter: IrFunction, originalGetter: IrSimpleFunction): IrBlockBody { - val originalReturnType = originalGetter.returnType as? IrSimpleTypeImpl - ?: throw IllegalStateException("Unsupported return type ${originalGetter.returnType}") - return DeclarationIrBuilder(context, getter.symbol, - originalGetter.startOffset, originalGetter.endOffset).irBlockBody { - val returnType = originalReturnType.getStateFlowValueTypeOrNull()?.typeOrNull - ?: throw IllegalStateException("Unsupported StateFlow value type $originalReturnType") - val valueGetter = stateFlowValueProperty.owner.getter?.symbol - ?: throw IllegalStateException("Couldn't find StateFlow value getter") - +irReturn(irCall(valueGetter, returnType).apply { - dispatchReceiver = callOriginalFunction(getter, originalGetter) - }) - } - } - - private fun createNativeReplayCacheBody(getter: IrFunction, originalGetter: IrSimpleFunction): IrBlockBody { - val originalReturnType = originalGetter.returnType as? IrSimpleTypeImpl - ?: throw IllegalStateException("Unsupported return type ${originalGetter.returnType}") - return DeclarationIrBuilder(context, getter.symbol, - originalGetter.startOffset, originalGetter.endOffset).irBlockBody { - val valueType = originalReturnType.getSharedFlowValueTypeOrNull()?.typeOrNull as? IrSimpleTypeImpl - ?: throw IllegalStateException("Unsupported StateFlow value type $originalReturnType") - val returnType = IrSimpleTypeImpl(listClass, false, listOf(valueType), emptyList()) - val valueGetter = sharedFlowReplayCacheProperty.owner.getter?.symbol - ?: throw IllegalStateException("Couldn't find StateFlow value getter") - +irReturn(irCall(valueGetter, returnType).apply { - dispatchReceiver = callOriginalFunction(getter, originalGetter) - }) - } - } - - override fun visitFunctionNew(declaration: IrFunction): IrStatement { - if (declaration.isFakeOverride || - !nameGenerator.isNativeName(declaration.name) || - !declaration.isNativeCoroutinesFunction || - declaration.body != null - ) return super.visitFunctionNew(declaration) - val originalName = nameGenerator.createOriginalName(declaration.name) - val originalFunction = declaration.parentAsClass.functions.single { - it.name == originalName && it.needsNativeFunction && it.valueParameters.areSameAs(declaration.valueParameters) - } - declaration.body = createNativeBody(declaration, originalFunction) - return super.visitFunctionNew(declaration) - } - - private fun createNativeBody(declaration: IrFunction, originalFunction: IrSimpleFunction): IrBlockBody { - val originalReturnType = originalFunction.returnType as? IrSimpleTypeImpl - ?: throw IllegalStateException("Unsupported return type ${originalFunction.returnType}") - return DeclarationIrBuilder(context, declaration.symbol, - originalFunction.startOffset, originalFunction.endOffset).irBlockBody { - // Call original function - var returnType = originalReturnType - var call: IrExpression = callOriginalFunction(declaration, originalFunction) - // Call nativeCoroutineScope - val nativeCoroutineScope = callNativeCoroutineScope(declaration) - // Convert Flow types to NativeFlow - val flowValueType = returnType.getFlowValueTypeOrNull() - if (flowValueType != null) { - val valueType = flowValueType.typeOrNull - ?: throw IllegalStateException("Unsupported Flow value type $flowValueType") - returnType = IrSimpleTypeImpl( - nativeFlowFunction.owner.returnType.classifierOrFail, - false, - listOf(flowValueType), - emptyList() - ) - call = irCall(nativeFlowFunction, returnType).apply { - putTypeArgument(0, valueType) - putValueArgument(0, nativeCoroutineScope) - extensionReceiver = call - } - } - // Convert suspend functions to NativeSuspend - if (originalFunction.isSuspend) { - val lambdaType = IrSimpleTypeImpl( - nativeSuspendFunction.owner.valueParameters[1].type.classifierOrFail, - false, - listOf(returnType), - emptyList() - ) - val lambda = irLambda(true, lambdaType, returnType) { - +irReturn(call) - } - returnType = IrSimpleTypeImpl( - nativeSuspendFunction.owner.returnType.classifierOrFail, - false, - listOf(returnType), - emptyList() - ) - call = irCall(nativeSuspendFunction, returnType).apply { - putTypeArgument(0, lambda.function.returnType) - putValueArgument(0, nativeCoroutineScope) - putValueArgument(1, lambda) - } - } - +irReturn(call) - } - } - - private fun IrBuilderWithScope.callOriginalFunction(function: IrFunction, originalFunction: IrFunction) = - irCall(originalFunction).apply { - dispatchReceiver = function.dispatchReceiverParameter?.let { irGet(it) } - extensionReceiver = function.extensionReceiverParameter?.let { irGet(it) } - passTypeArgumentsFrom(function) - function.valueParameters.forEachIndexed { index, parameter -> - putValueArgument(index, irGet(parameter)) - } - } - - private fun IrBuilderWithScope.callNativeCoroutineScope(function: IrFunction): IrExpression { - val parentClass = function.parentClassOrNull ?: return irNull() - val nativeCoroutineScopeProperty = parentClass.declarations - .mapNotNull { it as? IrProperty } - .firstOrNull { it.isNativeCoroutineScope } ?: return irNull() - val getter = nativeCoroutineScopeProperty.getter ?: return irNull() - if (getter.extensionReceiverParameter != null) - throw UnsupportedOperationException("NativeCoroutineScope property can't be an extension property") - return irCall(getter).apply { - dispatchReceiver = function.dispatchReceiverParameter?.let { irGet(it) } - } - } -} diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt deleted file mode 100644 index 2ad95da1..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt +++ /dev/null @@ -1,261 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler - -import com.rickclephas.kmp.nativecoroutines.compiler.utils.* -import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.descriptors.annotations.Annotations -import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl -import org.jetbrains.kotlin.descriptors.impl.PropertyGetterDescriptorImpl -import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl -import org.jetbrains.kotlin.incremental.components.NoLookupLocation -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.resolve.BindingContext -import org.jetbrains.kotlin.resolve.descriptorUtil.* -import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension -import org.jetbrains.kotlin.resolve.lazy.descriptors.AbstractLazyMemberScope -import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyClassMemberScope -import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter -import org.jetbrains.kotlin.resolve.scopes.MemberScope -import org.jetbrains.kotlin.types.KotlinType -import java.util.ArrayList - -internal class KmpNativeCoroutinesSyntheticResolveExtension( - private val nameGenerator: NameGenerator -): SyntheticResolveExtension { - - // We need the user declared functions. Unfortunately there doesn't seem to be an official way for that. - // Instead, we'll use reflection to use the same code the compiler is using. - // https://github.com/JetBrains/kotlin/blob/fe8f7cfcae3b33ba7ee5d06cd45e5e68f3c421a8/compiler/frontend/src/org/jetbrains/kotlin/resolve/lazy/descriptors/LazyClassMemberScope.kt#L64 - @Suppress("UNCHECKED_CAST") - private fun ClassDescriptor.getDeclarations(): MutableSet { - val memberScope = unsubstitutedMemberScope - if (memberScope !is LazyClassMemberScope) return mutableSetOf() - return AbstractLazyMemberScope::class.java.declaredMethods - .first { it.name == "computeDescriptorsFromDeclaredElements" } - .apply { isAccessible = true } - .invoke(memberScope, DescriptorKindFilter.ALL, MemberScope.ALL_NAME_FILTER, - NoLookupLocation.WHEN_GET_ALL_DESCRIPTORS) as MutableSet - } - - // We need two extensions so that we can check for recursion calls with `syntheticResolveExtensionClassName`. - class RecursiveCallSyntheticResolveExtension : SyntheticResolveExtension - - private val syntheticResolveExtensionClassName = - "org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension\$Companion\$getInstance\$1" - - // Our SyntheticResolveExtension might be called recursively, either by some other plugin - // or by our own SyntheticResolveExtension when we try to check some return types. - // To prevent these loops we'll check the current stacktrace for the `syntheticResolveExtensionClassName`. - // It should only occur once, if it occurs more than once then this is a recursive call. - private fun isRecursiveCall(): Boolean { - Throwable().stackTrace.fold(0) { acc, element -> - when (element.className) { - syntheticResolveExtensionClassName -> acc + 1 - else -> acc - }.also { if (it > 1) return true } - }.also { if (it < 1) throw IllegalStateException("Not called from SyntheticResolveExtension") } - return false - } - - private fun ClassDescriptor.getDeclaredProperties(): List = - getDeclarations().mapNotNull { it as? PropertyDescriptor } - - override fun getSyntheticPropertiesNames(thisDescriptor: ClassDescriptor): List = - if (isRecursiveCall()) emptyList() - else thisDescriptor.getDeclaredProperties() - .filter { it.needsNativeProperty } - .flatMap { - val hasStateFlowType = it.hasStateFlowType - val hasSharedFlowType = it.hasSharedFlowType - listOfNotNull( - nameGenerator.createNativeName(it.name), - if (hasStateFlowType) nameGenerator.createNativeValueName(it.name) else null, - if (hasSharedFlowType && !hasStateFlowType) nameGenerator.createNativeReplayCacheName(it.name) else null - ) - } - .distinct() - - override fun generateSyntheticProperties( - thisDescriptor: ClassDescriptor, - name: Name, - bindingContext: BindingContext, - fromSupertypes: ArrayList, - result: MutableSet - ) { - if (result.isNotEmpty()) return - val isNativeName = nameGenerator.isNativeName(name) - val isNativeValueName = nameGenerator.isNativeValueName(name) - val isNativeReplayCacheName = nameGenerator.isNativeReplayCacheName(name) - if (!isNativeName && !isNativeValueName && !isNativeReplayCacheName) return - val originalName = nameGenerator.createOriginalName(name) - result += thisDescriptor.getDeclaredProperties() - .filter { it.name == originalName && it.needsNativeProperty } - .map { - when { - isNativeName -> createNativePropertyDescriptor(thisDescriptor, it, name) - isNativeValueName -> createNativeValuePropertyDescriptor(thisDescriptor, it, name) - isNativeReplayCacheName -> createNativeReplayCachePropertyDescriptor(thisDescriptor, it, name) - else -> throw IllegalStateException("Unsupported property type") - } - } - } - - private fun createNativePropertyDescriptor( - thisDescriptor: ClassDescriptor, - coroutinesPropertyDescriptor: PropertyDescriptor, - name: Name - ): PropertyDescriptor { - val valueType = coroutinesPropertyDescriptor.getFlowValueTypeOrNull()?.type - ?: throw IllegalStateException("Coroutines property doesn't have a value type") - val type = thisDescriptor.module.getExpandedNativeFlowType(valueType) - return createPropertyDescriptor(thisDescriptor, coroutinesPropertyDescriptor.visibility, - name, type, coroutinesPropertyDescriptor.dispatchReceiverParameter, - coroutinesPropertyDescriptor.extensionReceiverParameter, - coroutinesPropertyDescriptor.contextReceiverParameters - ) - } - - private fun createNativeValuePropertyDescriptor( - thisDescriptor: ClassDescriptor, - coroutinesPropertyDescriptor: PropertyDescriptor, - name: Name - ): PropertyDescriptor { - val valueType = coroutinesPropertyDescriptor.getStateFlowValueTypeOrNull()?.type - ?: throw IllegalStateException("Coroutines property doesn't have a value type") - return createPropertyDescriptor(thisDescriptor, coroutinesPropertyDescriptor.visibility, - name, valueType, coroutinesPropertyDescriptor.dispatchReceiverParameter, - coroutinesPropertyDescriptor.extensionReceiverParameter, - coroutinesPropertyDescriptor.contextReceiverParameters - ) - } - - private fun createNativeReplayCachePropertyDescriptor( - thisDescriptor: ClassDescriptor, - coroutinesPropertyDescriptor: PropertyDescriptor, - name: Name - ): PropertyDescriptor { - val valueType = coroutinesPropertyDescriptor.getSharedFlowValueTypeOrNull()?.type - ?: throw IllegalStateException("Coroutines property doesn't have a value type") - val type = thisDescriptor.module.createListType(valueType) - return createPropertyDescriptor(thisDescriptor, coroutinesPropertyDescriptor.visibility, - name, type, coroutinesPropertyDescriptor.dispatchReceiverParameter, - coroutinesPropertyDescriptor.extensionReceiverParameter, - coroutinesPropertyDescriptor.contextReceiverParameters - ) - } - - private fun createPropertyDescriptor( - containingDeclaration: ClassDescriptor, - visibility: DescriptorVisibility, - name: Name, - outType: KotlinType, - dispatchReceiverParameter: ReceiverParameterDescriptor?, - extensionReceiverParameter: ReceiverParameterDescriptor?, - contextReceiverParameters: List - ): PropertyDescriptor = PropertyDescriptorImpl.create( - containingDeclaration, - Annotations.EMPTY, - if (containingDeclaration.kind.isInterface) Modality.OPEN else Modality.FINAL, - visibility, - false, - name, - CallableMemberDescriptor.Kind.SYNTHESIZED, - SourceElement.NO_SOURCE, - false, - false, - false, - false, - false, - false - ).apply { - setType( - outType, - emptyList(), - dispatchReceiverParameter, - extensionReceiverParameter, - contextReceiverParameters - ) - initialize( - PropertyGetterDescriptorImpl( - this, - Annotations.EMPTY, - modality, - visibility, - false, - false, - false, - CallableMemberDescriptor.Kind.SYNTHESIZED, - null, - SourceElement.NO_SOURCE - ).apply { initialize(outType) }, - null - ) - } - - private fun ClassDescriptor.getDeclaredFunctions(): List = - getDeclarations().mapNotNull { it as? SimpleFunctionDescriptor } - - override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List = - if (isRecursiveCall()) emptyList() - else thisDescriptor.getDeclaredFunctions() - .filter { it.needsNativeFunction } - .map { nameGenerator.createNativeName(it.name) } - .distinct() - - override fun generateSyntheticMethods( - thisDescriptor: ClassDescriptor, - name: Name, - bindingContext: BindingContext, - fromSupertypes: List, - result: MutableCollection - ) { - if (!nameGenerator.isNativeName(name) || result.isNotEmpty()) return - val originalName = nameGenerator.createOriginalName(name) - result += thisDescriptor.getDeclaredFunctions() - .filter { it.name == originalName && it.needsNativeFunction } - .map { createNativeFunctionDescriptor(thisDescriptor, it, name) } - } - - private fun createNativeFunctionDescriptor( - thisDescriptor: ClassDescriptor, - coroutinesFunctionDescriptor: SimpleFunctionDescriptor, - name: Name - ): SimpleFunctionDescriptor = - SimpleFunctionDescriptorImpl.create( - thisDescriptor, - Annotations.EMPTY, - name, - CallableMemberDescriptor.Kind.SYNTHESIZED, - SourceElement.NO_SOURCE - ).apply { - val typeParameters = coroutinesFunctionDescriptor.typeParameters.copyFor(this) - val extensionReceiverParameter = coroutinesFunctionDescriptor.extensionReceiverParameter - ?.copyFor(this, typeParameters) - val valueParameters = coroutinesFunctionDescriptor.valueParameters - .copyFor(this, typeParameters) - - var returnType = coroutinesFunctionDescriptor.returnType - ?: throw IllegalStateException("Coroutines function doesn't have a return type") - returnType = returnType.replaceFunctionGenerics(coroutinesFunctionDescriptor, typeParameters) - - // Convert Flow types to NativeFlow - val flowValueType = coroutinesFunctionDescriptor.getFlowValueTypeOrNull()?.type - ?.replaceFunctionGenerics(coroutinesFunctionDescriptor, typeParameters) - if (flowValueType != null) - returnType = thisDescriptor.module.getExpandedNativeFlowType(flowValueType) - - // Convert suspend function to NativeSuspend - if (coroutinesFunctionDescriptor.isSuspend) - returnType = thisDescriptor.module.getExpandedNativeSuspendType(returnType) - - initialize( - extensionReceiverParameter, - coroutinesFunctionDescriptor.dispatchReceiverParameter, - coroutinesFunctionDescriptor.contextReceiverParameters, - typeParameters, - valueParameters, - returnType, - if (thisDescriptor.kind.isInterface) Modality.OPEN else Modality.FINAL, - coroutinesFunctionDescriptor.visibility - ) - } -} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesFunction.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesFunction.kt index 8d26035a..aae9015e 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesFunction.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesFunction.kt @@ -2,10 +2,8 @@ package com.rickclephas.kmp.nativecoroutines.compiler.utils import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor import org.jetbrains.kotlin.descriptors.effectiveVisibility -import org.jetbrains.kotlin.ir.declarations.IrFunction -import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction -internal val SimpleFunctionDescriptor.isCoroutinesFunction: Boolean +private val SimpleFunctionDescriptor.isCoroutinesFunction: Boolean get() = !name.isSpecial && (isSuspend || hasFlowReturnType) internal val SimpleFunctionDescriptor.needsNativeFunction: Boolean @@ -13,15 +11,3 @@ internal val SimpleFunctionDescriptor.needsNativeFunction: Boolean !hasIgnoreAnnotation && overriddenDescriptors.size != 1 && isCoroutinesFunction - -internal val IrSimpleFunction.isCoroutinesFunction: Boolean - get() = !name.isSpecial && (isSuspend || returnType.isFlowType) - -internal val IrSimpleFunction.needsNativeFunction: Boolean - get() = visibility.isPublicAPI && - !hasIgnoreAnnotation && - overriddenSymbols.size != 1 && - isCoroutinesFunction - -internal val IrFunction.isNativeCoroutinesFunction: Boolean - get() = !name.isSpecial && (returnType.isNativeSuspend || returnType.isNativeFlow) \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesProperty.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesProperty.kt index f9e81e7f..29e231d4 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesProperty.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/CoroutinesProperty.kt @@ -2,9 +2,8 @@ package com.rickclephas.kmp.nativecoroutines.compiler.utils import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.descriptors.effectiveVisibility -import org.jetbrains.kotlin.ir.declarations.IrProperty -internal val PropertyDescriptor.isCoroutinesProperty: Boolean +private val PropertyDescriptor.isCoroutinesProperty: Boolean get() = !name.isSpecial && hasFlowType internal val PropertyDescriptor.needsNativeProperty: Boolean @@ -12,12 +11,3 @@ internal val PropertyDescriptor.needsNativeProperty: Boolean !hasIgnoreAnnotation && overriddenDescriptors.size != 1 && isCoroutinesProperty - -internal val IrProperty.isCoroutinesProperty: Boolean - get() = !name.isSpecial && (getter?.returnType?.isFlowType == true) - -internal val IrProperty.needsNativeProperty: Boolean - get() = visibility.isPublicAPI && - !hasIgnoreAnnotation && - overriddenSymbols.size != 1 && - isCoroutinesProperty \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Flow.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Flow.kt index a84e1937..66bcb115 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Flow.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Flow.kt @@ -1,16 +1,11 @@ package com.rickclephas.kmp.nativecoroutines.compiler.utils import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.ir.types.* -import org.jetbrains.kotlin.ir.util.getAllSubstitutedSupertypes -import org.jetbrains.kotlin.ir.util.substitute -import org.jetbrains.kotlin.ir.util.superTypes import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.TypeConstructor -import org.jetbrains.kotlin.types.TypeProjection import org.jetbrains.kotlin.types.typeUtil.supertypes private val flowFqName = FqName("kotlinx.coroutines.flow.Flow") @@ -20,7 +15,7 @@ private fun ModuleDescriptor.findFlowClassifier(): ClassifierDescriptor = findClassifierAcrossModuleDependencies(flowClassId) ?: throw NoSuchElementException("Couldn't find Flow classifier") -internal fun KotlinType.isFlowType(typeConstructor: TypeConstructor): Boolean = +private fun KotlinType.isFlowType(typeConstructor: TypeConstructor): Boolean = constructor == typeConstructor || supertypes().any { it.constructor == typeConstructor } @@ -30,34 +25,3 @@ internal val SimpleFunctionDescriptor.hasFlowReturnType: Boolean internal val PropertyDescriptor.hasFlowType: Boolean get() = type.isFlowType(module.findFlowClassifier().typeConstructor) - -private fun KotlinType.getFlowValueTypeOrNull(typeConstructor: TypeConstructor): TypeProjection? { - if (constructor == typeConstructor) { - return arguments.first() - } - return supertypes().firstOrNull { - it.constructor == typeConstructor - }?.arguments?.first() -} - -internal fun SimpleFunctionDescriptor.getFlowValueTypeOrNull( - typeConstructor: TypeConstructor = module.findFlowClassifier().typeConstructor -) = returnType?.getFlowValueTypeOrNull(typeConstructor) - -internal fun PropertyDescriptor.getFlowValueTypeOrNull( - typeConstructor: TypeConstructor = module.findFlowClassifier().typeConstructor -) = type.getFlowValueTypeOrNull(typeConstructor) - -internal val IrType.isFlowType: Boolean - get() = classFqName == flowFqName || superTypes().any { it.isFlowType } - -internal fun IrType.getFlowValueTypeOrNull(fqName: FqName = flowFqName): IrTypeArgument? { - if (this !is IrSimpleType) return null - if (classFqName == fqName) return arguments.first() - val irClass = getClass() ?: return null - val superTypes = getAllSubstitutedSupertypes(irClass) - return superTypes.firstOrNull { it.classFqName == fqName } - ?.substitute(irClass.typeParameters, arguments.map { it.typeOrNull!! }) - ?.let { it as? IrSimpleType } - ?.arguments?.first() -} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrLambda.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrLambda.kt deleted file mode 100644 index 242bfda2..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrLambda.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder -import org.jetbrains.kotlin.descriptors.DescriptorVisibilities -import org.jetbrains.kotlin.ir.builders.* -import org.jetbrains.kotlin.ir.builders.declarations.buildFun -import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin -import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression -import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin -import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl -import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl -import org.jetbrains.kotlin.name.Name - -internal fun IrBuilderWithScope.irLambda( - isSuspend: Boolean, - lambdaType: IrSimpleTypeImpl, - returnType: IrSimpleTypeImpl, - body: IrBlockBodyBuilder.() -> Unit -): IrFunctionExpression { - val lambdaFunction = context.irFactory.buildFun { - startOffset = this@irLambda.startOffset - endOffset = this@irLambda.endOffset - name = Name.special("") - visibility = DescriptorVisibilities.LOCAL - origin = IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA - this.isSuspend = isSuspend - this.returnType = returnType - }.apply { - parent = this@irLambda.parent - this.body = DeclarationIrBuilder(context, symbol).irBlockBody(body = body) - } - return IrFunctionExpressionImpl( - startOffset, - endOffset, - lambdaType, - lambdaFunction, - IrStatementOrigin.LAMBDA - ) -} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrValueParameter.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrValueParameter.kt deleted file mode 100644 index 17d1a0be..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrValueParameter.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.ir.declarations.IrTypeParameter -import org.jetbrains.kotlin.ir.declarations.IrValueParameter -import org.jetbrains.kotlin.ir.symbols.FqNameEqualityChecker -import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol -import org.jetbrains.kotlin.ir.types.IrSimpleType -import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.IrTypeProjection -import org.jetbrains.kotlin.ir.types.classifierOrNull - -internal fun IrValueParameter.isSameAs(other: IrValueParameter): Boolean = - index == other.index && name == other.name && isCrossinline == other.isCrossinline && - isNoinline == other.isNoinline && isHidden == other.isHidden && hasSameTypeAs(other) - -internal fun List.areSameAs(others: List): Boolean { - if (size != others.size) return false - for (i in indices) { - if (!this[i].isSameAs(others[i])) return false - } - return true -} - -private fun IrValueParameter.hasSameTypeAs(other: IrValueParameter): Boolean { - val paramParent = parent - val otherParamParent = other.parent - fun IrType.isSameTypeAs(other: IrType): Boolean { - // Check the types if this isn't a generic type or if the generic type isn't on the function - val classifier = classifierOrNull - val typeParameter = classifier?.owner as? IrTypeParameter - if (classifier !is IrTypeParameterSymbol || typeParameter?.parent != paramParent) { - // If these aren't simple types just use the equals check - if (this !is IrSimpleType || other !is IrSimpleType) - return this == other - // else run the same code from the equals check except for the arguments - // https://github.com/JetBrains/kotlin/blob/01a24fdc0821a5c598a9bbd54774198730c044bd/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/types/impl/IrSimpleTypeImpl.kt#L27 - if (!FqNameEqualityChecker.areEqual(this.classifier, other.classifier) || - nullability != other.nullability || arguments.size != other.arguments.size) - return false - // Check if the arguments are the same - for (i in arguments.indices) { - val argument = arguments[i] - val otherArgument = other.arguments[i] - // For type projections we need to check the type manually - // else we'll just use the equals check - if (argument is IrTypeProjection && otherArgument is IrTypeProjection) { - // Run the same code from the equals check except for the type - // https://github.com/JetBrains/kotlin/blob/486c6b3c15dfa245693c3df2c58c8353d75deddb/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/types/impl/IrSimpleTypeImpl.kt#L116 - if (argument.variance != otherArgument.variance || - !argument.type.isSameTypeAs(otherArgument.type) - ) return false - } else { - if (argument != otherArgument) return false - } - } - return true - } - // else make sure the other type is also a generic type on the function - val otherTypeParameter = other.classifierOrNull?.owner as? IrTypeParameter - if (otherTypeParameter?.parent != otherParamParent) return false - // Check if the generic types are equal - if (typeParameter.name != otherTypeParameter.name || - typeParameter.index != otherTypeParameter.index || - typeParameter.isReified != otherTypeParameter.isReified || - typeParameter.variance != otherTypeParameter.variance - ) return false - // Check if the super types are equal - val superTypes = typeParameter.superTypes - val otherSuperTypes = otherTypeParameter.superTypes - if (superTypes.size != otherSuperTypes.size) return false - for (i in superTypes.indices) - if (!superTypes[i].isSameTypeAs(otherSuperTypes[i])) return false - return true - } - return type.isSameTypeAs(other.type) && run { - val varargElementType = varargElementType - val otherVarargElementType = other.varargElementType - if (varargElementType != null && otherVarargElementType != null) - return@run varargElementType.isSameTypeAs(otherVarargElementType) - return@run varargElementType == null && otherVarargElementType == null - } -} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/KotlinType.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/KotlinType.kt deleted file mode 100644 index 6f26e06c..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/KotlinType.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.KotlinTypeFactory -import org.jetbrains.kotlin.types.TypeConstructor - -internal fun KotlinType.replaceFunctionGenerics( - oldContainingDeclaration: DeclarationDescriptor, - newTypeParameters: List -): KotlinType { - fun KotlinType.replaceFunctionGenerics(): KotlinType? { - // Replace the type constructor - var typeConstructor: TypeConstructor? = null - val typeParameter = constructor.declarationDescriptor as? TypeParameterDescriptor - if (typeParameter != null && typeParameter.containingDeclaration == oldContainingDeclaration) - typeConstructor = newTypeParameters.first { it.index == typeParameter.index }.typeConstructor - // Replace the arguments - var shouldReplaceArguments = false - val arguments = arguments.map { projection -> - projection.type.replaceFunctionGenerics()?.let { - shouldReplaceArguments = true - projection.replaceType(it) - } ?: projection - } - // Only create a new type if something was replaced - if (typeConstructor == null && !shouldReplaceArguments) return null - return KotlinTypeFactory.simpleType(attributes, typeConstructor ?: constructor, arguments, isMarkedNullable) - } - return this.replaceFunctionGenerics() ?: this -} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt deleted file mode 100644 index 3bd028f3..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.ir.symbols.IrClassSymbol -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.KotlinTypeFactory -import org.jetbrains.kotlin.types.typeUtil.asTypeProjection - -private val listClassId = ClassId.topLevel(FqName("kotlin.collections.List")) - -internal fun ModuleDescriptor.findListClassifier(): ClassifierDescriptor = - findClassifierAcrossModuleDependencies(listClassId) - ?: throw NoSuchElementException("Couldn't find List classifier") - -internal fun ModuleDescriptor.createListType(valueType: KotlinType): KotlinType = - KotlinTypeFactory.simpleType(findListClassifier().defaultType, arguments = listOf(valueType.asTypeProjection())) - -internal fun IrPluginContext.referenceListClass(): IrClassSymbol = - referenceClass(listClassId) ?: throw NoSuchElementException("Couldn't find List symbol") diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NameGenerator.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NameGenerator.kt deleted file mode 100644 index 21fca46e..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NameGenerator.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.name.Name - -internal class NameGenerator(private val suffix: String) { - - fun createNativeName(name: Name): Name = - Name.identifier("${name.identifier}$suffix") - - fun isNativeName(name: Name): Boolean = - !name.isSpecial && name.identifier.endsWith(suffix) - - fun createNativeValueName(name: Name): Name = - Name.identifier("${name.identifier}${suffix}Value") - - fun isNativeValueName(name: Name): Boolean = - !name.isSpecial && name.identifier.endsWith("${suffix}Value") - - fun createNativeReplayCacheName(name: Name): Name = - Name.identifier("${name.identifier}${suffix}ReplayCache") - - fun isNativeReplayCacheName(name: Name): Boolean = - !name.isSpecial && name.identifier.endsWith("${suffix}ReplayCache") - - private val regex = Regex("${suffix}(Value|ReplayCache)?$") - - fun createOriginalName(nativeName: Name): Name = - Name.identifier(nativeName.identifier.replace(regex, "")) -} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineScope.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineScope.kt deleted file mode 100644 index a0ad9679..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineScope.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.ir.declarations.IrProperty -import org.jetbrains.kotlin.ir.types.classFqName -import org.jetbrains.kotlin.name.FqName - -private val nativeCoroutineScopeFqName = FqName("com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope") - -val IrProperty.isNativeCoroutineScope: Boolean - get() = getter?.returnType?.isCoroutineScopeType == true && - annotations.any { it.type.classFqName == nativeCoroutineScopeFqName } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutinesIgnore.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutinesIgnore.kt index 7f81179d..b1d5130c 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutinesIgnore.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutinesIgnore.kt @@ -1,14 +1,9 @@ package com.rickclephas.kmp.nativecoroutines.compiler.utils import org.jetbrains.kotlin.descriptors.annotations.Annotated -import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer -import org.jetbrains.kotlin.ir.interpreter.hasAnnotation import org.jetbrains.kotlin.name.FqName private val nativeCoroutinesIgnoreFqName = FqName("com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore") internal val Annotated.hasIgnoreAnnotation: Boolean get() = annotations.findAnnotation(nativeCoroutinesIgnoreFqName) != null - -internal val IrAnnotationContainer.hasIgnoreAnnotation: Boolean - get() = hasAnnotation(nativeCoroutinesIgnoreFqName) \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeFlow.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeFlow.kt deleted file mode 100644 index 74c15b69..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeFlow.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.descriptors.TypeAliasDescriptor -import org.jetbrains.kotlin.descriptors.findTypeAliasAcrossModuleDependencies -import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol -import org.jetbrains.kotlin.ir.types.IrSimpleType -import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.util.kotlinFqName -import org.jetbrains.kotlin.name.CallableId -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.KotlinTypeFactory.computeExpandedType -import org.jetbrains.kotlin.types.typeUtil.asTypeProjection - -private val typeAliasFqName = FqName("com.rickclephas.kmp.nativecoroutines.NativeFlow") -private val typeAliasClassId = ClassId.topLevel(typeAliasFqName) - -internal val IrType.isNativeFlow: Boolean - get() = (this as? IrSimpleType)?.abbreviation?.typeAlias?.owner?.kotlinFqName == typeAliasFqName - -internal fun ModuleDescriptor.findNativeFlowTypeAlias(): TypeAliasDescriptor = - findTypeAliasAcrossModuleDependencies(typeAliasClassId) - ?: throw NoSuchElementException("Couldn't find NativeFlow typealias") - -internal fun ModuleDescriptor.getExpandedNativeFlowType(valueType: KotlinType): KotlinType = - findNativeFlowTypeAlias().computeExpandedType(listOf(valueType.asTypeProjection())) - -private val functionCallableId = CallableId( - FqName("com.rickclephas.kmp.nativecoroutines"), - Name.identifier("asNativeFlow") -) - -internal fun IrPluginContext.referenceNativeFlowFunction(): IrSimpleFunctionSymbol = - referenceFunctions(functionCallableId).single() diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeSuspend.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeSuspend.kt deleted file mode 100644 index f6162fb1..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeSuspend.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.descriptors.TypeAliasDescriptor -import org.jetbrains.kotlin.descriptors.findTypeAliasAcrossModuleDependencies -import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol -import org.jetbrains.kotlin.ir.types.IrSimpleType -import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.util.kotlinFqName -import org.jetbrains.kotlin.name.CallableId -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.KotlinTypeFactory.computeExpandedType -import org.jetbrains.kotlin.types.typeUtil.asTypeProjection - -private val typeAliasFqName = FqName("com.rickclephas.kmp.nativecoroutines.NativeSuspend") -private val typeAliasClassId = ClassId.topLevel(typeAliasFqName) - -internal val IrType.isNativeSuspend: Boolean - get() = (this as? IrSimpleType)?.abbreviation?.typeAlias?.owner?.kotlinFqName == typeAliasFqName - -internal fun ModuleDescriptor.findNativeSuspendTypeAlias(): TypeAliasDescriptor = - findTypeAliasAcrossModuleDependencies(typeAliasClassId) - ?: throw NoSuchElementException("Couldn't find NativeSuspend typealias") - -internal fun ModuleDescriptor.getExpandedNativeSuspendType(valueType: KotlinType): KotlinType = - findNativeSuspendTypeAlias().computeExpandedType(listOf(valueType.asTypeProjection())) - -private val functionCallableId = CallableId( - FqName("com.rickclephas.kmp.nativecoroutines"), - Name.identifier("nativeSuspend") -) - -internal fun IrPluginContext.referenceNativeSuspendFunction(): IrSimpleFunctionSymbol = - referenceFunctions(functionCallableId).single() diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/ReceiverParameterDescriptor.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/ReceiverParameterDescriptor.kt deleted file mode 100644 index d401fe4d..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/ReceiverParameterDescriptor.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.descriptors.CallableDescriptor -import org.jetbrains.kotlin.descriptors.ReceiverParameterDescriptor -import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor -import org.jetbrains.kotlin.descriptors.impl.ReceiverParameterDescriptorImpl - -internal fun ReceiverParameterDescriptor.copyFor( - newContainingDeclaration: CallableDescriptor, - newTypeParameters: List -): ReceiverParameterDescriptor { - val value = value.let { it.replaceType(it.type.replaceFunctionGenerics(containingDeclaration, newTypeParameters)) } - return ReceiverParameterDescriptorImpl(newContainingDeclaration, value, annotations) -} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/SharedFlow.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/SharedFlow.kt deleted file mode 100644 index bc1a3801..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/SharedFlow.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.ClassifierDescriptor -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.descriptors.PropertyDescriptor -import org.jetbrains.kotlin.descriptors.findClassifierAcrossModuleDependencies -import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol -import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.IrTypeArgument -import org.jetbrains.kotlin.name.CallableId -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.resolve.descriptorUtil.module -import org.jetbrains.kotlin.types.TypeProjection - -private val sharedFlowFqName = FqName("kotlinx.coroutines.flow.SharedFlow") -private val sharedFlowClassId = ClassId.topLevel(sharedFlowFqName) - -private fun ModuleDescriptor.findSharedFlowClassifier(): ClassifierDescriptor = - findClassifierAcrossModuleDependencies(sharedFlowClassId) - ?: throw NoSuchElementException("Couldn't find SharedFlow classifier") - -internal val PropertyDescriptor.hasSharedFlowType: Boolean - get() = type.isFlowType(module.findSharedFlowClassifier().typeConstructor) - -internal fun PropertyDescriptor.getSharedFlowValueTypeOrNull(): TypeProjection? = - getFlowValueTypeOrNull(module.findSharedFlowClassifier().typeConstructor) - -internal fun IrType.getSharedFlowValueTypeOrNull(): IrTypeArgument? = - getFlowValueTypeOrNull(sharedFlowFqName) - -private val sharedFlowReplayCacheCallableId = CallableId(sharedFlowClassId, Name.identifier("replayCache")) - -internal fun IrPluginContext.referenceSharedFlowReplayCacheProperty(): IrPropertySymbol = - referenceProperties(sharedFlowReplayCacheCallableId).single() diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/StateFlow.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/StateFlow.kt deleted file mode 100644 index 8e4f4a46..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/StateFlow.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol -import org.jetbrains.kotlin.ir.types.* -import org.jetbrains.kotlin.name.CallableId -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.resolve.descriptorUtil.module -import org.jetbrains.kotlin.types.TypeProjection - -private val stateFlowFqName = FqName("kotlinx.coroutines.flow.StateFlow") -private val stateFlowClassId = ClassId.topLevel(stateFlowFqName) - -private fun ModuleDescriptor.findStateFlowClassifier(): ClassifierDescriptor = - findClassifierAcrossModuleDependencies(stateFlowClassId) - ?: throw NoSuchElementException("Couldn't find StateFlow classifier") - -internal val PropertyDescriptor.hasStateFlowType: Boolean - get() = type.isFlowType(module.findStateFlowClassifier().typeConstructor) - -internal fun PropertyDescriptor.getStateFlowValueTypeOrNull(): TypeProjection? = - getFlowValueTypeOrNull(module.findStateFlowClassifier().typeConstructor) - -internal fun IrType.getStateFlowValueTypeOrNull(): IrTypeArgument? = - getFlowValueTypeOrNull(stateFlowFqName) - -private val stateFlowValueCallableId = CallableId(stateFlowClassId, Name.identifier("value")) - -internal fun IrPluginContext.referenceStateFlowValueProperty(): IrPropertySymbol = - referenceProperties(stateFlowValueCallableId).single() diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/TypeParameterDescriptor.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/TypeParameterDescriptor.kt deleted file mode 100644 index 8861e0d5..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/TypeParameterDescriptor.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.descriptors.SourceElement -import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor -import org.jetbrains.kotlin.descriptors.impl.TypeParameterDescriptorImpl - -internal fun TypeParameterDescriptor.copyFor( - newContainingDeclaration: DeclarationDescriptor -): TypeParameterDescriptor = - TypeParameterDescriptorImpl.createForFurtherModification( - newContainingDeclaration, - annotations, - isReified, - variance, - name, - index, - SourceElement.NO_SOURCE, - storageManager - ).apply { - this@copyFor.upperBounds.forEach(::addUpperBound) - setInitialized() - } - -internal fun List.copyFor( - newContainingDeclaration: DeclarationDescriptor -): List = - map { it.copyFor(newContainingDeclaration) } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/ValueParameterDescriptor.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/ValueParameterDescriptor.kt deleted file mode 100644 index 826ade45..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/ValueParameterDescriptor.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.descriptors.CallableDescriptor -import org.jetbrains.kotlin.descriptors.SourceElement -import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor -import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor -import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl - -internal fun ValueParameterDescriptor.copyFor( - newContainingDeclaration: CallableDescriptor, - newTypeParameters: List -): ValueParameterDescriptor = - ValueParameterDescriptorImpl( - newContainingDeclaration, - null, - index, - annotations, - name, - type.replaceFunctionGenerics(containingDeclaration, newTypeParameters), - false, // TODO: Support and copy default values when they are exported to ObjC - isCrossinline, - isNoinline, - varargElementType?.replaceFunctionGenerics(containingDeclaration, newTypeParameters), - SourceElement.NO_SOURCE - ) - -internal fun List.copyFor( - newContainingDeclaration: CallableDescriptor, - newTypeParameters: List -): List = - map { it.copyFor(newContainingDeclaration, newTypeParameters) } \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt index e391701e..e341df2a 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt @@ -3,7 +3,7 @@ package com.rickclephas.kmp.nativecoroutines /** * A callback with a single argument. * - * We don't want the Swift code to known how to get the [Unit] object, so we'll provide it as the second argument. + * We don't want the Swift code to know how to get the [Unit] object, so we'll provide it as the second argument. * This way Swift can just return the value that it received without knowing what it is/how to get it. */ typealias NativeCallback = (T, Unit) -> Unit @@ -11,4 +11,19 @@ typealias NativeCallback = (T, Unit) -> Unit /** * Invokes the callback with the specified [value]. */ -internal inline operator fun NativeCallback.invoke(value: T) = invoke(value, Unit) \ No newline at end of file +internal inline operator fun NativeCallback.invoke(value: T) = + invoke(value, Unit) + +/** + * A callback with two arguments. + * + * We don't want the Swift code to know how to get the [Unit] object, so we'll provide it as the third argument. + * This way Swift can just return the value that it received without knowing what it is/how to get it. + */ +typealias NativeCallback2 = (T1, T2, Unit) -> Unit + +/** + * Invokes the callback with the specified [value1] and [value2]. + */ +internal inline operator fun NativeCallback2.invoke(value1: T1, value2: T2) = + invoke(value1, value2, Unit) diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt index df7b519a..bbfff75b 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt @@ -5,14 +5,20 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine /** * A function that collects a [Flow] via callbacks. * - * The function takes an `onItem` and `onComplete` callback + * The function takes an `onItem`, `onComplete` and `onCancelled` callback * and returns a cancellable that can be used to cancel the collection. */ -typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback) -> NativeCancellable +typealias NativeFlow = ( + onItem: NativeCallback2 Unit>, + onComplete: NativeCallback, + onCancelled: NativeCallback +) -> NativeCancellable /** * Creates a [NativeFlow] for this [Flow]. @@ -23,10 +29,16 @@ typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback */ fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { val coroutineScope = scope ?: defaultCoroutineScope - return (collect@{ onItem: NativeCallback, onComplete: NativeCallback -> + return (collect@{ onItem: NativeCallback2 Unit>, + onComplete: NativeCallback, + onCancelled: NativeCallback -> val job = coroutineScope.launch { try { - collect { onItem(it) } + collect { + suspendCoroutine { cont -> + onItem(it) { cont.resume(Unit) } + } + } onComplete(null) } catch (e: CancellationException) { // CancellationExceptions are handled by the invokeOnCompletion @@ -39,7 +51,7 @@ fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onComplete(cause.asNativeError()) + onCancelled(cause.asNativeError()) } return@collect job.asNativeCancellable() }) diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt index 6b46a8ca..94013239 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt @@ -7,20 +7,26 @@ import kotlinx.coroutines.launch /** * A function that awaits a suspend function via callbacks. * - * The function takes an `onResult` and `onError` callback + * The function takes an `onResult`, `onError` and `onCancelled` callback * and returns a cancellable that can be used to cancel the suspend function. */ -typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallback) -> NativeCancellable +typealias NativeSuspend = ( + onResult: NativeCallback, + onError: NativeCallback, + onCancelled: NativeCallback +) -> NativeCancellable /** * Creates a [NativeSuspend] for the provided suspend [block]. * * @param scope the [CoroutineScope] to run the [block] in, or `null` to use the [defaultCoroutineScope]. - * @param block the suspend block to await. + * @param block the suspend-block to await. */ fun nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): NativeSuspend { val coroutineScope = scope ?: defaultCoroutineScope - return (collect@{ onResult: NativeCallback, onError: NativeCallback -> + return (collect@{ onResult: NativeCallback, + onError: NativeCallback, + onCancelled: NativeCallback -> val job = coroutineScope.launch { try { onResult(block()) @@ -35,7 +41,7 @@ fun nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): Na job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onError(cause.asNativeError()) + onCancelled(cause.asNativeError()) } return@collect job.asNativeCancellable() }) diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt index a5240f73..fc81797c 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt @@ -19,4 +19,24 @@ class NativeCallbackTests { assertEquals(1, invokeCount, "NativeCallback should have been invoked once") assertSame(value, receivedValue, "Received value should be the same as the send value") } + + @Test + fun ensureInvoked2() { + var invokeCount = 0 + var receivedValue1: RandomValue? = null + var receivedValue2: RandomValue? = null + val callback: NativeCallback2 = callback@{ value1, value2, unit -> + receivedValue1 = value1 + receivedValue2 = value2 + invokeCount++ + // This isn't required in Kotlin, but it is in Swift, so we'll test it anyway + return@callback unit + } + val value1 = RandomValue() + val value2 = RandomValue() + callback(value1, value2) + assertEquals(1, invokeCount, "NativeCallback should have been invoked once") + assertSame(value1, receivedValue1, "Received value 1 should be the same as send value 1") + assertSame(value2, receivedValue2, "Received value 2 should be the same as send value 2") + } } \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt index 70b2092a..88fa4c6e 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt @@ -14,12 +14,16 @@ class NativeFlowTests { val flow = flow { } val nativeFlow = flow.asNativeFlow(this) var completionCount = 0 - nativeFlow({ _, _ -> }, { error, _ -> + var cancellationCount = 0 + nativeFlow({ _, _, _ -> }, { error, _ -> assertNull(error, "Flow should complete without an error") completionCount++ + }, { _, _ -> + cancellationCount++ }) advanceUntilIdle() assertEquals(1, completionCount, "Completion callback should be called once") + assertEquals(0, cancellationCount, "Cancellation callback shouldn't be called") } @Test @@ -28,14 +32,18 @@ class NativeFlowTests { val flow = flow { throw exception } val nativeFlow = flow.asNativeFlow(this) var completionCount = 0 - nativeFlow({ _, _ -> }, { error, _ -> + var cancellationCount = 0 + nativeFlow({ _, _, _ -> }, { error, _ -> assertNotNull(error, "Flow should complete with an error") val kotlinException = error.kotlinCause assertSame(exception, kotlinException, "Kotlin exception should be the same exception") completionCount++ + }, { _, _ -> + cancellationCount++ }) advanceUntilIdle() assertEquals(1, completionCount, "Completion callback should be called once") + assertEquals(0, cancellationCount, "Cancellation callback shouldn't be called") } @Test @@ -44,10 +52,11 @@ class NativeFlowTests { val flow = flow { values.forEach { emit(it) } } val nativeFlow = flow.asNativeFlow(this) var receivedValueCount = 0 - nativeFlow({ value, _ -> + nativeFlow({ value, next, _ -> assertSame(values[receivedValueCount], value, "Received incorrect value") receivedValueCount++ - }, { _, _ -> }) + next() + }, { _, _ -> }, { _, _ -> }) advanceUntilIdle() assertEquals( values.size, @@ -61,15 +70,19 @@ class NativeFlowTests { val flow = MutableSharedFlow() val nativeFlow = flow.asNativeFlow(this) var completionCount = 0 - val cancel = nativeFlow({ _, _ -> }, { error, _ -> - assertNotNull(error, "Flow should complete with an error") + var cancellationCount = 0 + val cancel = nativeFlow({ _, _, _ -> }, { _, _ -> + completionCount++ + }, { error, _ -> + assertNotNull(error, "Flow should complete with a cancellation error") val exception = error.kotlinCause assertIs(exception, "Error should contain CancellationException") - completionCount++ + cancellationCount++ }) delay(100) // Gives the collection some time to start cancel() advanceUntilIdle() - assertEquals(1, completionCount, "Completion callback should be called once") + assertEquals(1, cancellationCount, "Cancellation callback should be called once") + assertEquals(0, completionCount, "Completion callback shouldn't be called") } } diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt index be622b87..0159187d 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt @@ -24,15 +24,19 @@ class NativeSuspendTests { val nativeSuspend = nativeSuspend(this) { delayAndReturn(100, value) } var receivedResultCount = 0 var receivedErrorCount = 0 + var receivedCancellationCount = 0 nativeSuspend({ receivedValue, _ -> assertSame(value, receivedValue, "Received incorrect value") receivedResultCount++ }, { _, _ -> receivedErrorCount++ + }, { _, _ -> + receivedCancellationCount++ }) advanceUntilIdle() assertEquals(1, receivedResultCount, "Result callback should be called once") assertEquals(0, receivedErrorCount, "Error callback shouldn't be called") + assertEquals(0, receivedCancellationCount, "Cancellation callback shouldn't be called") } @Test @@ -41,17 +45,20 @@ class NativeSuspendTests { val nativeSuspend = nativeSuspend(this) { delayAndThrow(100, exception) } var receivedResultCount = 0 var receivedErrorCount = 0 + var receivedCancellationCount = 0 nativeSuspend({ _, _ -> receivedResultCount++ }, { error, _ -> - assertNotNull(error, "Function should complete with an error") val kotlinException = error.kotlinCause assertSame(exception, kotlinException, "Kotlin exception should be the same exception") receivedErrorCount++ + }, { _, _ -> + receivedCancellationCount++ }) advanceUntilIdle() assertEquals(1, receivedErrorCount, "Error callback should be called once") assertEquals(0, receivedResultCount, "Result callback shouldn't be called") + assertEquals(0, receivedCancellationCount, "Cancellation callback shouldn't be called") } @Test @@ -59,18 +66,21 @@ class NativeSuspendTests { val nativeSuspend = nativeSuspend(this) { delayAndReturn(5_000, RandomValue()) } var receivedResultCount = 0 var receivedErrorCount = 0 + var receivedCancellationCount = 0 val cancel = nativeSuspend({ _, _ -> receivedResultCount++ + }, { _, _ -> + receivedErrorCount++ }, { error, _ -> - assertNotNull(error, "Function should complete with an error") val exception = error.kotlinCause assertIs(exception, "Error should contain CancellationException") - receivedErrorCount++ + receivedCancellationCount++ }) delay(100) // Gives the function some time to start cancel() advanceUntilIdle() - assertEquals(1, receivedErrorCount, "Error callback should be called once") + assertEquals(1, receivedCancellationCount, "Cancellation callback should be called once") + assertEquals(0, receivedErrorCount, "Error callback shouldn't be called") assertEquals(0, receivedResultCount, "Result callback shouldn't be called") } } diff --git a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt index f2431b34..b9bf8134 100644 --- a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt +++ b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt @@ -1,5 +1,34 @@ package com.rickclephas.kmp.nativecoroutines.gradle open class KmpNativeCoroutinesExtension { + /** + * The suffix used to generate the native coroutine function and property names. + */ var suffix: String = "Native" -} \ No newline at end of file + /** + * The suffix used to generate the native coroutine file names. + * Note: defaults to [suffix] when `null`. + */ + var fileSuffix: String? = null + /** + * The suffix used to generate the `StateFlow` value property names, + * or `null` to remove the value properties. + */ + var flowValueSuffix: String? = "Value" + /** + * The suffix used to generate the `SharedFlow` replayCache property names, + * or `null` to remove the replayCache properties. + */ + var flowReplayCacheSuffix: String? = "ReplayCache" + /** + * The suffix used to generate the native state property names. + * @see com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState + */ + var stateSuffix: String = "Value" + /** + * The suffix used to generate the `StateFlow` flow property names, + * or `null` to remove the flow properties. + * @see com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState + */ + var stateFlowSuffix: String? = "Flow" +} diff --git a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt index d2444414..818fb043 100644 --- a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt +++ b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt @@ -8,26 +8,50 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget @Suppress("unused") class KmpNativeCoroutinesPlugin: KotlinCompilerPluginSupportPlugin { + companion object { + private val KotlinTarget.isKmpNativeCoroutinesTarget: Boolean + get() = this is KotlinNativeTarget && konanTarget.family.isAppleFamily + + private fun Project.setKSPArguments(block: ((String, String) -> Unit) -> Unit) { + val ksp = extensions.getByName("ksp") + val argMethod = Class.forName("com.google.devtools.ksp.gradle.KspExtension") + .getDeclaredMethod("arg", String::class.java, String::class.java) + block { key, value -> argMethod.invoke(ksp, key, value) } + } + } override fun apply(target: Project) { target.extensions.create("nativeCoroutines", KmpNativeCoroutinesExtension::class.java) - target.afterEvaluate { - val sourceSet = target.extensions.getByType(KotlinMultiplatformExtension::class.java) - .sourceSets.getByName("commonMain") - target.configurations.getByName(sourceSet.implementationConfigurationName).dependencies.apply { - add(target.dependencies.create("com.rickclephas.kmp:kmp-nativecoroutines-core:$VERSION")) - add(target.dependencies.create("com.rickclephas.kmp:kmp-nativecoroutines-annotations:$VERSION")) + target.afterEvaluate { project -> + val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val commonMainSourceSet = kotlin.sourceSets.getByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) + project.configurations.getByName(commonMainSourceSet.implementationConfigurationName).dependencies.apply { + add(project.dependencies.create("com.rickclephas.kmp:kmp-nativecoroutines-core:$VERSION")) + add(project.dependencies.create("com.rickclephas.kmp:kmp-nativecoroutines-annotations:$VERSION")) + } + kotlin.targets.filter { it.isKmpNativeCoroutinesTarget }.map { target -> + "ksp${target.targetName.replaceFirstChar { it.uppercaseChar() }}" + }.forEach { + project.dependencies.add(it, "com.rickclephas.kmp:kmp-nativecoroutines-ksp:$VERSION") + } + val nativeCoroutines = project.extensions.getByType(KmpNativeCoroutinesExtension::class.java) + project.setKSPArguments { arg -> + arg("nativeCoroutines.suffix", nativeCoroutines.suffix) + nativeCoroutines.fileSuffix?.let { arg("nativeCoroutines.fileSuffix", it) } + nativeCoroutines.flowValueSuffix?.let { arg("nativeCoroutines.flowValueSuffix", it) } + nativeCoroutines.flowReplayCacheSuffix?.let { arg("nativeCoroutines.flowReplayCacheSuffix", it) } + arg("nativeCoroutines.stateSuffix", nativeCoroutines.stateSuffix) + nativeCoroutines.stateFlowSuffix?.let { arg("nativeCoroutines.stateFlowSuffix", it) } } } } override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = - kotlinCompilation.target.let { it is KotlinNativeTarget && it.konanTarget.family.isAppleFamily } + kotlinCompilation.target.isKmpNativeCoroutinesTarget override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { val project = kotlinCompilation.target.project - val extension = project.extensions.findByType(KmpNativeCoroutinesExtension::class.java) - ?: KmpNativeCoroutinesExtension() + val extension = project.extensions.getByType(KmpNativeCoroutinesExtension::class.java) return project.provider { listOf(SubpluginOption("suffix", extension.suffix)) } @@ -40,4 +64,4 @@ class KmpNativeCoroutinesPlugin: KotlinCompilerPluginSupportPlugin { override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact("com.rickclephas.kmp", "kmp-nativecoroutines-compiler-embeddable", VERSION) -} \ No newline at end of file +} diff --git a/kmp-nativecoroutines-ksp/build.gradle.kts b/kmp-nativecoroutines-ksp/build.gradle.kts new file mode 100644 index 00000000..8aa40235 --- /dev/null +++ b/kmp-nativecoroutines-ksp/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + @Suppress("DSL_SCOPE_VIOLATION") + alias(libs.plugins.kotlin.jvm) + `kmp-nativecoroutines-publish` +} + +dependencies { + implementation(libs.ksp.api) + implementation(libs.kotlinpoet) + implementation(libs.kotlinpoet.ksp) + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlinCompileTesting.ksp) + testImplementation(libs.kotlinx.coroutines.core) + testImplementation(project(":kmp-nativecoroutines-annotations")) +} + +tasks.compileKotlin.configure { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = listOf("-Xjvm-default=all") + } +} + +val sourcesJar by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets.main.get().allSource) +} + +publishing { + publications { + create("maven") { + from(components["kotlin"]) + artifact(sourcesJar) + } + } +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/AnnotationSpecs.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/AnnotationSpecs.kt new file mode 100644 index 00000000..a66c1d85 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/AnnotationSpecs.kt @@ -0,0 +1,58 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.rickclephas.kmp.nativecoroutines.ksp.kotlinpoet.canonicalClassName +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ksp.toAnnotationSpec + +internal fun Sequence.toAnnotationSpecs( + objCName: String? = null, + ignoredAnnotationNames: Set = emptySet() +): List { + val annotationSpecs = mutableListOf() + var objCNameAnnotation: AnnotationSpec? = null + for (annotation in this) { + if (annotation.isObjCName) { + objCNameAnnotation = annotation.toObjCNameAnnotationSpec(objCName ?: "") + continue + } + annotation.toAnnotationSpec() + .takeUnless { it.typeName.canonicalClassName in ignoredAnnotationNames } + ?.let(annotationSpecs::add) + } + if (objCNameAnnotation == null && objCName != null) { + objCNameAnnotation = ObjCNameAnnotationSpec(objCName, null) + } + objCNameAnnotation?.let(annotationSpecs::add) + return annotationSpecs +} + +private val KSAnnotation.isObjCName: Boolean get() { + val classDeclaration = annotationType.resolve() as? KSClassDeclaration ?: return false + return classDeclaration.packageName.asString() == objCNameAnnotationClassName.packageName && + classDeclaration.simpleName.asString() == objCNameAnnotationClassName.simpleName +} + +private fun KSAnnotation.toObjCNameAnnotationSpec(objCName: String): AnnotationSpec { + var name: String? = null + var swiftName: String? = null + for (argument in arguments) { + val value = argument.value as? String ?: continue + when (argument.name!!.getShortName()) { + "name" -> name = value + "swiftName" -> swiftName = value + } + } + if (name == null) name = objCName + return ObjCNameAnnotationSpec(name, swiftName) +} + +@Suppress("FunctionName") +private fun ObjCNameAnnotationSpec(name: String?, swiftName: String?): AnnotationSpec = + AnnotationSpec.builder(objCNameAnnotationClassName).apply { + if (name != null) + addMember("%N = %S", "name", name) + if (swiftName != null) + addMember("%N = %S", "swiftName", swiftName) + }.build() diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CoroutineScopeProvider.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CoroutineScopeProvider.kt new file mode 100644 index 00000000..2848c4fb --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CoroutineScopeProvider.kt @@ -0,0 +1,121 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.* +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.ksp.toClassName + +internal class CoroutineScopeProvider( + private val logger: KSPLogger +) { + + companion object { + private val KSClassDeclaration.scopePropertyKey: String + get() = "class://${toClassName().canonicalName}" + + private val KSFile.scopePropertyKey: String + get() = "file://${filePath}" + } + + data class ScopeProperty( + val code: String, + val codeArg: List, + val containingFile: KSFile? + ) { + companion object { + val DEFAULT = ScopeProperty("null", emptyList(), null) + + fun viewModelScope(containingFile: KSFile): ScopeProperty = ScopeProperty("%N.%M", listOf( + "viewModelScope", + MemberName("com.rickclephas.kmm.viewmodel", "coroutineScope", true) + ), containingFile) + } + } + + private val scopeProperties = mutableMapOf() + + fun process(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(nativeCoroutineScopeAnnotationName).forEach { symbol -> + if (symbol !is KSPropertyDeclaration) { + logger.warn("Unsupported symbol type", symbol) + return@forEach + } + process(symbol) + } + } + + private fun process(property: KSPropertyDeclaration) { + val classDeclaration = property.parentDeclaration as? KSClassDeclaration + val file = property.containingFile ?: run { + logger.error("Property isn't contained in a source file", property) + return + } + val code: String + val codeArg: Any + if (classDeclaration == null) { + val isExtension = property.extensionReceiver != null + codeArg = MemberName(property.packageName.asString(), property.simpleName.asString(), isExtension) + code = "%M" + } else { + codeArg = property.simpleName.asString() + code = "%N" + } + val scopeProperty = ScopeProperty(code, listOf(codeArg), file) + if (classDeclaration == null) { + if (scopeProperties.putIfAbsent(file.scopePropertyKey, scopeProperty) != null) { + logger.warn("Ignoring duplicate scope property", property) + } + } else { + if (scopeProperties.putIfAbsent(classDeclaration.scopePropertyKey, scopeProperty) != null) { + logger.warn("Ignoring duplicate scope property", property) + } + } + } + + fun getScopeProperty(declaration: KSDeclaration): ScopeProperty? { + val classDeclaration = declaration.parentDeclaration as? KSClassDeclaration + if (classDeclaration != null) { + val classScopeProperty = classDeclaration.let(::getScopeProperty) ?: return null + if (classScopeProperty != ScopeProperty.DEFAULT) return classScopeProperty + } + val file = declaration.containingFile ?: run { + logger.error("Declaration isn't contained in a source file", declaration) + return null + } + scopeProperties[file.scopePropertyKey]?.let { return it } + if (classDeclaration == null) { + val receiverClassDeclaration = when (declaration) { + is KSPropertyDeclaration -> declaration.extensionReceiver + is KSFunctionDeclaration -> declaration.extensionReceiver + else -> { + logger.warn("Unsupported declaration type", declaration) + null + } + }?.resolve()?.declaration as? KSClassDeclaration + if (receiverClassDeclaration != null) { + val receiverScopeProperty = receiverClassDeclaration.let(::getScopeProperty) ?: return null + if (receiverScopeProperty != ScopeProperty.DEFAULT) return receiverScopeProperty + } + } + return ScopeProperty.DEFAULT + } + + private fun getScopeProperty(classDeclaration: KSClassDeclaration): ScopeProperty? { + var containingFile: KSFile = classDeclaration.containingFile ?: return ScopeProperty.DEFAULT + scopeProperties[classDeclaration.scopePropertyKey]?.let { return it } + classDeclaration.getAllSuperTypes().forEach { superType -> + if (superType.isError) return null + val superClassDeclaration = superType.declaration as? KSClassDeclaration ?: return@forEach + scopeProperties[superClassDeclaration.scopePropertyKey]?.let { return it } + // If this class is a KMMViewModel, use the ViewModelScope + if (superClassDeclaration.isKMMViewModel()) return ScopeProperty.viewModelScope(containingFile) + containingFile = superClassDeclaration.containingFile ?: containingFile + } + return ScopeProperty.DEFAULT + } + + private fun KSClassDeclaration.isKMMViewModel(): Boolean = + packageName.asString() == "com.rickclephas.kmm.viewmodel" && simpleName.asString() == "KMMViewModel" +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesOptions.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesOptions.kt new file mode 100644 index 00000000..df717a6a --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesOptions.kt @@ -0,0 +1,12 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +internal class KmpNativeCoroutinesOptions( + options: Map +) { + val suffix = options["nativeCoroutines.suffix"] ?: error("Missing required option: suffix") + val fileSuffix = options["nativeCoroutines.fileSuffix"] ?: suffix + val flowValueSuffix = options["nativeCoroutines.flowValueSuffix"] + val flowReplayCacheSuffix = options["nativeCoroutines.flowReplayCacheSuffix"] + val stateSuffix = options["nativeCoroutines.stateSuffix"] ?: error("Missing required option: stateSuffix") + val stateFlowSuffix = options["nativeCoroutines.stateFlowSuffix"] +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesSymbolProcessor.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesSymbolProcessor.kt new file mode 100644 index 00000000..5ab401d5 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesSymbolProcessor.kt @@ -0,0 +1,77 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.validate +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.ksp.writeTo + +internal class KmpNativeCoroutinesSymbolProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, + private val options: KmpNativeCoroutinesOptions +): SymbolProcessor { + + private val coroutineScopeProvider = CoroutineScopeProvider(logger) + + private val fileSpecBuilders = mutableMapOf() + + private fun KSFile.getFileSpecBuilder(): FileSpec.Builder = fileSpecBuilders.getOrPut(filePath) { + FileSpec.builder(packageName.asString(), "${fileName.removeSuffix(".kt")}${options.fileSuffix}") + } + + override fun process(resolver: Resolver): List { + coroutineScopeProvider.process(resolver) + val deferredSymbols = mutableListOf() + resolver.getSymbolsWithAnnotation(nativeCoroutinesAnnotationName).forEach { symbol -> + when (symbol) { + is KSPropertyDeclaration -> symbol.takeUnless(::processProperty)?.let(deferredSymbols::add) + is KSFunctionDeclaration -> symbol.takeUnless(::processFunction)?.let(deferredSymbols::add) + else -> logger.warn("Unsupported symbol type", symbol) + } + } + resolver.getSymbolsWithAnnotation(nativeCoroutinesStateAnnotationName).forEach { symbol -> + when (symbol) { + is KSPropertyDeclaration -> symbol.takeUnless(::processStateProperty)?.let(deferredSymbols::add) + else -> logger.warn("Unsupported symbol type", symbol) + } + } + if (deferredSymbols.isEmpty()) { + fileSpecBuilders.forEach { (_, fileSpecBuilder) -> + fileSpecBuilder.build().writeTo(codeGenerator, true) + } + fileSpecBuilders.clear() + } + return deferredSymbols + } + + private fun processProperty(property: KSPropertyDeclaration, asState: Boolean = false): Boolean { + if (!property.validate()) return false + val file = property.containingFile + if (file == null) { + logger.error("Property isn't contained in a source file", property) + return true + } + val scopeProperty = coroutineScopeProvider.getScopeProperty(property) ?: return false + val propertySpecs = property.toNativeCoroutinesPropertySpecs(scopeProperty, options, asState) ?: return false + val fileSpecBuilder = file.getFileSpecBuilder() + propertySpecs.forEach(fileSpecBuilder::addProperty) + return true + } + + private fun processStateProperty(property: KSPropertyDeclaration): Boolean = + processProperty(property, true) + + private fun processFunction(function: KSFunctionDeclaration): Boolean { + if (!function.validate()) return false + val file = function.containingFile + if (file == null) { + logger.error("Function isn't contained in a source file", function) + return true + } + val scopeProperty = coroutineScopeProvider.getScopeProperty(function) ?: return false + val funSpec = function.toNativeCoroutinesFunSpec(scopeProperty, options) ?: return false + file.getFileSpecBuilder().addFunction(funSpec) + return true + } +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesSymbolProcessorProvider.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesSymbolProcessorProvider.kt new file mode 100644 index 00000000..2efc9c21 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/KmpNativeCoroutinesSymbolProcessorProvider.kt @@ -0,0 +1,12 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class KmpNativeCoroutinesSymbolProcessorProvider: SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + val options = KmpNativeCoroutinesOptions(environment.options) + return KmpNativeCoroutinesSymbolProcessor(environment.codeGenerator, environment.logger, options) + } +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/Names.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/Names.kt new file mode 100644 index 00000000..24bbd565 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/Names.kt @@ -0,0 +1,20 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.MemberName + +private const val packageName = "com.rickclephas.kmp.nativecoroutines" + +internal const val nativeCoroutinesAnnotationName = "$packageName.NativeCoroutines" +internal const val nativeCoroutinesStateAnnotationName = "$packageName.NativeCoroutinesState" +internal const val nativeCoroutineScopeAnnotationName = "$packageName.NativeCoroutineScope" + +internal val nativeSuspendMemberName = MemberName(packageName, "nativeSuspend") +internal val nativeSuspendClassName = ClassName(packageName, "NativeSuspend") + +internal val asNativeFlowMemberName = MemberName(packageName, "asNativeFlow") +internal val nativeFlowClassName = ClassName(packageName, "NativeFlow") + +internal val runMemberName = MemberName("kotlin", "run") +internal val objCNameAnnotationClassName = ClassName("kotlin.native", "ObjCName") +internal const val throwsAnnotationName = "kotlin.Throws" diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesFunSpec.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesFunSpec.kt new file mode 100644 index 00000000..91a42a02 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesFunSpec.kt @@ -0,0 +1,95 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.google.devtools.ksp.symbol.* +import com.rickclephas.kmp.nativecoroutines.ksp.kotlinpoet.* +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.* + +internal fun KSFunctionDeclaration.toNativeCoroutinesFunSpec( + scopeProperty: CoroutineScopeProvider.ScopeProperty, + options: KmpNativeCoroutinesOptions +): FunSpec? { + val typeParameterResolver = getTypeParameterResolver() + val classDeclaration = parentDeclaration as? KSClassDeclaration + + val simpleName = simpleName.asString() + val builder = FunSpec.builder("$simpleName${options.suffix}") + docString?.trim()?.let(builder::addKdoc) + builder.addAnnotations(annotations.toAnnotationSpecs( + objCName = simpleName, + ignoredAnnotationNames = setOf(nativeCoroutinesAnnotationName, throwsAnnotationName) + )) + // TODO: Add context receivers once those are exported to ObjC + builder.addModifiers(KModifier.PUBLIC) + + classDeclaration?.typeParameters?.toTypeVariableNames(typeParameterResolver)?.let(builder::addTypeVariables) + val typeParameters = typeParameters.toTypeVariableNames(typeParameterResolver).also(builder::addTypeVariables) + + val extensionReceiver = extensionReceiver + var receiverParameter: ParameterSpec? = null + if (classDeclaration != null) { + builder.receiver(classDeclaration.toTypeName(typeParameterResolver)) + if (extensionReceiver != null) { + val type = extensionReceiver.toTypeName(typeParameterResolver) + receiverParameter = ParameterSpec.builder("receiver", type).build().also(builder::addParameter) + } + } else if (extensionReceiver != null) { + builder.receiver(extensionReceiver.toTypeName(typeParameterResolver)) + } + val parameters = parameters.toParameterSpecs(typeParameterResolver).also(builder::addParameters) + + val returnType = returnType?.getReturnType(typeParameterResolver) ?: return null + val isSuspend = modifiers.contains(Modifier.SUSPEND) + + var returnTypeName = when (returnType) { + is ReturnType.Flow -> nativeFlowClassName.parameterizedBy(returnType.valueType).copy(nullable = returnType.nullable) + else -> returnType.typeReference.toTypeName(typeParameterResolver) + } + if (isSuspend) { + returnTypeName = nativeSuspendClassName.parameterizedBy(returnTypeName) + } + returnTypeName = returnTypeName.copy(annotations = returnType.typeReference.annotations.toAnnotationSpecs()) + builder.returns(returnTypeName) + + val codeArgs = mutableListOf() + var code = when (classDeclaration) { + null -> { + val isExtension = extensionReceiver != null + codeArgs.add(MemberName(packageName.asString(), simpleName, isExtension)) + "%M" + } + else -> { + codeArgs.add(simpleName) + "%N" + } + } + if (typeParameters.isNotEmpty()) { + codeArgs.addAll(typeParameters.map { it.name }) + code += "<${typeParameters.joinToString { "%N" }}>" + } + codeArgs.addAll(parameters) + code += "(${parameters.joinToString { if (KModifier.VARARG in it.modifiers) "*%N" else "%N" }})" + if (receiverParameter != null) { + codeArgs.add(0, runMemberName) + codeArgs.add(1, receiverParameter) + code = "%M { %N.$code }" + } + if (returnType is ReturnType.Flow) { + codeArgs.add(asNativeFlowMemberName) + scopeProperty.codeArg.let(codeArgs::addAll) + if (returnType.nullable) code += "?" + code = "$code.%M(${scopeProperty.code})" + } + if (isSuspend) { + codeArgs.add(0, nativeSuspendMemberName) + scopeProperty.codeArg.let { codeArgs.addAll(1, it) } + code = "%M(${scopeProperty.code}) { $code }" + } + code = "return $code" + builder.addCode(code, *codeArgs.toTypedArray()) + + containingFile?.let(builder::addOriginatingKSFile) + scopeProperty.containingFile?.let(builder::addOriginatingKSFile) + return builder.build() +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesPropertySpecs.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesPropertySpecs.kt new file mode 100644 index 00000000..ce98cae0 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesPropertySpecs.kt @@ -0,0 +1,152 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.google.devtools.ksp.symbol.* +import com.rickclephas.kmp.nativecoroutines.ksp.kotlinpoet.* +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.* + +internal fun KSPropertyDeclaration.toNativeCoroutinesPropertySpecs( + scopeProperty: CoroutineScopeProvider.ScopeProperty, + options: KmpNativeCoroutinesOptions, + asState: Boolean = false +): List? { + val typeParameterResolver = getTypeParameterResolver() + val type = type.getReturnType(typeParameterResolver) ?: return null + if (type !is ReturnType.Flow) error("Only Flow properties are supported") + return buildList { + val flowSuffix = if (asState) options.stateFlowSuffix else options.suffix + if (flowSuffix != null) + add(toNativeCoroutinesPropertySpec(scopeProperty, flowSuffix, !asState, typeParameterResolver, type)) + val valueSuffix = if (asState) options.stateSuffix else options.flowValueSuffix + if (type is ReturnType.Flow.State && valueSuffix != null) + add(toNativeCoroutinesValuePropertySpec(valueSuffix, asState, typeParameterResolver, type)) + if (type is ReturnType.Flow.Shared && options.flowReplayCacheSuffix != null) + add(toNativeCoroutinesReplayCachePropertySpec(options.flowReplayCacheSuffix, typeParameterResolver, type)) + } +} + +private fun KSPropertyDeclaration.toNativeCoroutinesPropertySpec( + scopeProperty: CoroutineScopeProvider.ScopeProperty, + nameSuffix: String, + setObjCName: Boolean, + typeParameterResolver: TypeParameterResolver, + type: ReturnType.Flow +): PropertySpec { + var typeName: TypeName = nativeFlowClassName.parameterizedBy(type.valueType).copy(nullable = type.nullable) + typeName = typeName.copy(annotations = type.typeReference.annotations.toAnnotationSpecs()) + val simpleName = simpleName.asString() + val name = "$simpleName$nameSuffix" + val objCName = if (setObjCName) simpleName else null + return createPropertySpec(typeParameterResolver, name, objCName, typeName, { code, codeArgs -> + codeArgs.add(asNativeFlowMemberName) + scopeProperty.codeArg.let(codeArgs::addAll) + addCode("return $code${if(type.nullable) "?." else "."}%M(${scopeProperty.code})", *codeArgs.toTypedArray()) + }).apply { + scopeProperty.containingFile?.let(::addOriginatingKSFile) + }.build() +} + +private fun KSPropertyDeclaration.toNativeCoroutinesValuePropertySpec( + nameSuffix: String, + setObjCName: Boolean, + typeParameterResolver: TypeParameterResolver, + type: ReturnType.Flow.State +): PropertySpec { + var typeName = type.valueType.copy(annotations = type.typeReference.annotations.toAnnotationSpecs()) + if (type.nullable) typeName = typeName.copy(nullable = true) + val simpleName = simpleName.asString() + val name = "$simpleName$nameSuffix" + val objCName = if (setObjCName) simpleName else null + return createPropertySpec(typeParameterResolver, name, objCName, typeName, { code, codeArgs -> + addCode("return $code${if(type.nullable) "?." else "."}value", *codeArgs.toTypedArray()) + }, when (type.mutable) { + false -> null + else -> { code, codeArgs -> + addCode("$code${if(type.nullable) "?." else "."}value = value", *codeArgs.toTypedArray()) + } + }).build() +} + +private fun KSPropertyDeclaration.toNativeCoroutinesReplayCachePropertySpec( + nameSuffix: String, + typeParameterResolver: TypeParameterResolver, + type: ReturnType.Flow.Shared +): PropertySpec { + var typeName: TypeName = LIST.parameterizedBy(type.valueType).copy(nullable = type.nullable) + typeName = typeName.copy(annotations = type.typeReference.annotations.toAnnotationSpecs()) + val simpleName = simpleName.asString() + val name = "$simpleName$nameSuffix" + return createPropertySpec(typeParameterResolver, name, null, typeName, { code, codeArgs -> + addCode("return $code${if(type.nullable) "?." else "."}replayCache", *codeArgs.toTypedArray()) + }).build() +} + +private fun KSPropertyDeclaration.createPropertySpec( + typeParameterResolver: TypeParameterResolver, + name: String, + objCName: String?, + typeName: TypeName, + addGetterCode: FunSpec.Builder.(code: String, codeArgs: MutableList) -> Unit, + addSetterCode: (FunSpec.Builder.(code: String, codeArgs: MutableList) -> Unit)? = null +): PropertySpec.Builder { + val classDeclaration = parentDeclaration as? KSClassDeclaration + + val builder = PropertySpec.builder(name, typeName) + docString?.trim()?.let(builder::addKdoc) + builder.addAnnotations(annotations.toAnnotationSpecs( + objCName = objCName, + ignoredAnnotationNames = setOf( + nativeCoroutinesAnnotationName, + nativeCoroutinesStateAnnotationName, + throwsAnnotationName + ) + )) + // TODO: Add context receivers once those are exported to ObjC + builder.addModifiers(KModifier.PUBLIC) + + classDeclaration?.typeParameters?.toTypeVariableNames(typeParameterResolver)?.let(builder::addTypeVariables) + typeParameters.toTypeVariableNames(typeParameterResolver).let(builder::addTypeVariables) + + val extensionReceiver = extensionReceiver + if (classDeclaration != null) { + builder.receiver(classDeclaration.toTypeName(typeParameterResolver)) + if (extensionReceiver != null) error("Class extension properties aren't supported") + } else if (extensionReceiver != null) { + builder.receiver(extensionReceiver.toTypeName(typeParameterResolver)) + } + + val codeArgs = mutableListOf() + val code = when (classDeclaration) { + null -> { + val isExtension = extensionReceiver != null + codeArgs.add(MemberName(packageName.asString(), simpleName.asString(), isExtension)) + "%M" + } + else -> { + codeArgs.add(simpleName.asString()) + "%N" + } + } + + val getterBuilder = FunSpec.getterBuilder() + getter?.annotations?.toAnnotationSpecs( + ignoredAnnotationNames = setOf(throwsAnnotationName) + )?.let(getterBuilder::addAnnotations) + addGetterCode(getterBuilder, code, codeArgs) + builder.getter(getterBuilder.build()) + + if (addSetterCode != null) { + builder.mutable() + val setterBuilder = FunSpec.setterBuilder() + setter?.annotations?.toAnnotationSpecs( + ignoredAnnotationNames = setOf(throwsAnnotationName) + )?.let(setterBuilder::addAnnotations) + setterBuilder.addParameter("value", typeName) + addSetterCode(setterBuilder, code, codeArgs) + builder.setter(setterBuilder.build()) + } + + containingFile?.let(builder::addOriginatingKSFile) + return builder +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/ReturnType.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/ReturnType.kt new file mode 100644 index 00000000..ee07a836 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/ReturnType.kt @@ -0,0 +1,100 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.google.devtools.ksp.symbol.KSTypeReference +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.ksp.TypeParameterResolver +import com.squareup.kotlinpoet.ksp.toTypeName + +internal sealed class ReturnType { + abstract val typeReference: KSTypeReference + sealed class Flow: ReturnType() { + abstract val valueType: TypeName + abstract val nullable: Boolean + class State( + override val typeReference: KSTypeReference, + override val valueType: TypeName, + override val nullable: Boolean, + val mutable: Boolean + ): Flow() + class Shared( + override val typeReference: KSTypeReference, + override val valueType: TypeName, + override val nullable: Boolean + ): Flow() + class Generic( + override val typeReference: KSTypeReference, + override val valueType: TypeName, + override val nullable: Boolean + ): Flow() + } + class Other(override val typeReference: KSTypeReference): ReturnType() +} + +internal fun KSTypeReference.getReturnType( + typeParameterResolver: TypeParameterResolver, + typeParameterArguments: Map = emptyMap(), + typeIsMarkedNullable: Boolean? = null +): ReturnType? { + val type = resolve() + if (type.isError) return null + val classDeclaration = type.declaration as? KSClassDeclaration ?: return ReturnType.Other(this) + val typeArguments = type.arguments.map { typeArgument -> + val typeArgumentType = typeArgument.type?.resolve()?.takeUnless { it.isError } ?: return null + val typeArgumentDeclaration = typeArgumentType.declaration + if (typeArgumentDeclaration !is KSTypeParameter) return@map typeArgument.toTypeName(typeParameterResolver) + typeParameterArguments[typeArgumentDeclaration.name.getShortName()]?.let { + it.copy(nullable = it.isNullable || typeArgumentType.isMarkedNullable) + } ?: typeArgument.toTypeName(typeParameterResolver) + } + val isMarkedNullable = typeIsMarkedNullable ?: type.isMarkedNullable + if (classDeclaration.isMutableStateFlow()) + return ReturnType.Flow.State( + this, + typeArguments.first(), + isMarkedNullable, + true) + if (classDeclaration.isStateFlow()) + return ReturnType.Flow.State( + this, + typeArguments.first(), + isMarkedNullable, + false) + if (classDeclaration.isSharedFlow()) + return ReturnType.Flow.Shared( + this, + typeArguments.first(), + isMarkedNullable) + if (classDeclaration.isFlow()) + return ReturnType.Flow.Generic( + this, + typeArguments.first(), + isMarkedNullable) + val arguments = classDeclaration.typeParameters.map { it.name.getShortName() }.zip(typeArguments).toMap() + for (superType in classDeclaration.superTypes) { + val returnType = superType.getReturnType(typeParameterResolver, arguments, isMarkedNullable) + if (returnType == null || returnType is ReturnType.Flow) return returnType + } + return ReturnType.Other(this) +} + +private const val coroutinesPackageName = "kotlinx.coroutines.flow" + +private fun KSClassDeclaration.isMutableStateFlow(): Boolean = + packageName.asString() == coroutinesPackageName && simpleName.asString() == "MutableStateFlow" + +private fun KSClassDeclaration.isStateFlow(): Boolean = + packageName.asString() == coroutinesPackageName && simpleName.asString() == "StateFlow" + +private fun KSClassDeclaration.isSharedFlow(): Boolean { + if (packageName.asString() != coroutinesPackageName) return false + val simpleName = simpleName.asString() + return simpleName == "SharedFlow" || simpleName == "MutableSharedFlow" +} + +private fun KSClassDeclaration.isFlow(): Boolean { + if (packageName.asString() != coroutinesPackageName) return false + val simpleName = simpleName.asString() + return simpleName == "Flow" || simpleName == "AbstractFlow" +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/ParameterSpec.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/ParameterSpec.kt new file mode 100644 index 00000000..c1ab4054 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/ParameterSpec.kt @@ -0,0 +1,22 @@ +package com.rickclephas.kmp.nativecoroutines.ksp.kotlinpoet + +import com.google.devtools.ksp.symbol.KSValueParameter +import com.rickclephas.kmp.nativecoroutines.ksp.toAnnotationSpecs +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ksp.TypeParameterResolver +import com.squareup.kotlinpoet.ksp.toTypeName + +internal fun List.toParameterSpecs( + typeParameterResolver: TypeParameterResolver +) = map { parameter -> + val name = parameter.name?.asString() ?: "" + val type = parameter.type.toTypeName(typeParameterResolver) + val builder = ParameterSpec.builder(name, type) + builder.addAnnotations(parameter.annotations.toAnnotationSpecs()) + if (parameter.isVararg) { + builder.addModifiers(KModifier.VARARG) + } + // TODO: Add default value once those are exported to ObjC + builder.build() +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeName.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeName.kt new file mode 100644 index 00000000..2df3caf1 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeName.kt @@ -0,0 +1,26 @@ +package com.rickclephas.kmp.nativecoroutines.ksp.kotlinpoet + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.ksp.TypeParameterResolver +import com.squareup.kotlinpoet.ksp.toClassName + +internal fun KSClassDeclaration.toTypeName( + typeParameterResolver: TypeParameterResolver +): TypeName { + val className = toClassName() + val typeParams = typeParameters + return when { + typeParams.isEmpty() -> className + else -> className.parameterizedBy(typeParams.toTypeVariableNames(typeParameterResolver)) + } +} + +internal val TypeName.canonicalClassName: String? get() = when (this) { + is ClassName -> canonicalName + is ParameterizedTypeName -> rawType.canonicalName + else -> null +} diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeParameterResolver.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeParameterResolver.kt new file mode 100644 index 00000000..87a48196 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeParameterResolver.kt @@ -0,0 +1,8 @@ +package com.rickclephas.kmp.nativecoroutines.ksp.kotlinpoet + +import com.google.devtools.ksp.symbol.KSDeclaration +import com.squareup.kotlinpoet.ksp.TypeParameterResolver +import com.squareup.kotlinpoet.ksp.toTypeParameterResolver + +internal fun KSDeclaration.getTypeParameterResolver(): TypeParameterResolver = + typeParameters.toTypeParameterResolver(parentDeclaration?.getTypeParameterResolver()) diff --git a/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeVariableName.kt b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeVariableName.kt new file mode 100644 index 00000000..3f057505 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/kotlinpoet/TypeVariableName.kt @@ -0,0 +1,10 @@ +package com.rickclephas.kmp.nativecoroutines.ksp.kotlinpoet + +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.ksp.TypeParameterResolver +import com.squareup.kotlinpoet.ksp.toTypeVariableName + +internal fun List.toTypeVariableNames( + typeParameterResolver: TypeParameterResolver +): List = map { it.toTypeVariableName(typeParameterResolver) } diff --git a/kmp-nativecoroutines-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/kmp-nativecoroutines-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000..243cdbb1 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +com.rickclephas.kmp.nativecoroutines.ksp.KmpNativeCoroutinesSymbolProcessorProvider \ No newline at end of file diff --git a/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CompilationTests.kt b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CompilationTests.kt new file mode 100644 index 00000000..f4b05ea1 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CompilationTests.kt @@ -0,0 +1,52 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import com.tschuchort.compiletesting.* +import org.intellij.lang.annotations.Language +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.lang.Integer.max +import kotlin.test.assertEquals + +open class CompilationTests { + + @Rule @JvmField val temporaryFolder: TemporaryFolder = TemporaryFolder() + + protected fun runKspTest( + @Language("kotlin") inputContent: String, + @Language("kotlin") outputContent: String, + kspArgs: Map = mapOf( + "nativeCoroutines.suffix" to "Native", + "nativeCoroutines.flowValueSuffix" to "Value", + "nativeCoroutines.flowReplayCacheSuffix" to "ReplayCache", + "nativeCoroutines.stateSuffix" to "Value", + "nativeCoroutines.stateFlowSuffix" to "Flow" + ) + ) { + KotlinCompilation().apply { + workingDir = temporaryFolder.root + sources = listOf(SourceFile.new("Source.kt", "package test\n\n$inputContent")) + inheritClassPath = true + symbolProcessorProviders = listOf(KmpNativeCoroutinesSymbolProcessorProvider()) + this.kspArgs += kspArgs + assertCompile() + assertKspSourceFile("test/SourceNative.kt", "package test\n\n$outputContent") + } + } +} + +private fun KotlinCompilation.assertKspSourceFile(path: String, @Language("kotlin") contents: String) { + val file = kspSourcesDir.resolve("kotlin/$path") + assert(file.exists()) { "KSP source file <$path> doesn't exist." } + val expectedLines = contents.lines() + val actualLines = file.readLines() + for (i in 0 until max(expectedLines.size, actualLines.size)) { + assertEquals(expectedLines.getOrNull(i), actualLines.getOrNull(i), + "File: <$path> doesn't have equal contents on line: <${i + 1}>") + } +} + +private fun KotlinCompilation.assertCompile( + exitCode: KotlinCompilation.ExitCode = KotlinCompilation.ExitCode.OK +): KotlinCompilation.Result = compile().also { + assertEquals(exitCode, it.exitCode) +} diff --git a/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CoroutineScopeProviderTests.kt b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CoroutineScopeProviderTests.kt new file mode 100644 index 00000000..71066f44 --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/CoroutineScopeProviderTests.kt @@ -0,0 +1,237 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import org.junit.Test + +class CoroutineScopeProviderTests: CompilationTests() { + + @Test + fun fileScopeSuspendFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(): NativeSuspend = nativeSuspend(coroutineScope) { + returnSuspendValue() } + """.trimIndent()) + + @Test + fun fileScopeFlowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.flow.Flow + + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + + @NativeCoroutines + fun returnFlowValue(): Flow = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnFlowValue") + public fun returnFlowValueNative(): NativeFlow = + returnFlowValue().asNativeFlow(coroutineScope) + """.trimIndent()) + + @Test + fun fileScopeSuspendFlowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.flow.Flow + + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + + @NativeCoroutines + suspend fun returnSuspendFlowValue(): Flow = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendFlowValue") + public fun returnSuspendFlowValueNative(): NativeSuspend> = + nativeSuspend(coroutineScope) { returnSuspendFlowValue().asNativeFlow(coroutineScope) } + """.trimIndent()) + + @Test + fun fileScopeFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.flow.Flow + + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + + @NativeCoroutines + val globalFlow: Flow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "globalFlow") + public val globalFlowNative: NativeFlow + get() = globalFlow.asNativeFlow(coroutineScope) + """.trimIndent()) + + @Test + fun classScope() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + + class MyClass { + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun MyClass.returnSuspendValueNative(): NativeSuspend = nativeSuspend(coroutineScope) + { returnSuspendValue() } + """.trimIndent()) + + @Test + fun superClassScope() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + + open class SuperClass { + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + } + + class MyClass: SuperClass() { + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun MyClass.returnSuspendValueNative(): NativeSuspend = nativeSuspend(coroutineScope) + { returnSuspendValue() } + """.trimIndent()) + + @Test + fun subClassScope() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + + open class SuperClass { + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + } + + class MyClass: SuperClass() { + @NativeCoroutineScope + internal val myCoroutineScope = CoroutineScope(Dispatchers.Default) + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun MyClass.returnSuspendValueNative(): NativeSuspend = + nativeSuspend(myCoroutineScope) { returnSuspendValue() } + """.trimIndent()) + + @Test + fun receiverClassScope() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + + class MyClass { + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + } + + @NativeCoroutines + suspend fun MyClass.returnSuspendValue(): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun MyClass.returnSuspendValueNative(): NativeSuspend = nativeSuspend(coroutineScope) + { returnSuspendValue() } + """.trimIndent()) + + @Test + fun withoutContainedClassScope() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + + class MyOtherClass { + @NativeCoroutineScope + internal val coroutineScope = CoroutineScope(Dispatchers.Default) + } + + class MyClass { + @NativeCoroutines + suspend fun MyOtherClass.returnSuspendValue(): String = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + import kotlin.run + + @ObjCName(name = "returnSuspendValue") + public fun MyClass.returnSuspendValueNative(`receiver`: MyOtherClass): NativeSuspend = + nativeSuspend(null) { run { `receiver`.returnSuspendValue() } } + """.trimIndent()) +} diff --git a/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesFunSpecTests.kt b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesFunSpecTests.kt new file mode 100644 index 00000000..40292a4a --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesFunSpecTests.kt @@ -0,0 +1,438 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import org.junit.Ignore +import org.junit.Test + +class NativeCoroutinesFunSpecTests: CompilationTests() { + + @Test + fun suspendFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(): NativeSuspend = nativeSuspend(null) { + returnSuspendValue() } + """.trimIndent()) + + @Test + fun nullableSuspendFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @NativeCoroutines + suspend fun returnNullableSuspendValue(): String? = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnNullableSuspendValue") + public fun returnNullableSuspendValueNative(): NativeSuspend = nativeSuspend(null) { + returnNullableSuspendValue() } + """.trimIndent()) + + @Test + fun flowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + fun returnFlowValue(): Flow = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnFlowValue") + public fun returnFlowValueNative(): NativeFlow = returnFlowValue().asNativeFlow(null) + """.trimIndent()) + + @Test + fun customFlowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.AbstractFlow + + abstract class CustomFlow: AbstractFlow() + + @NativeCoroutines + fun returnCustomFlowValue(): CustomFlow = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.native.ObjCName + + @ObjCName(name = "returnCustomFlowValue") + public fun returnCustomFlowValueNative(): NativeFlow = + returnCustomFlowValue().asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableFlowValueFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + fun returnNullableFlowValue(): Flow = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnNullableFlowValue") + public fun returnNullableFlowValueNative(): NativeFlow = + returnNullableFlowValue().asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableFlowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + fun returnNullableFlow(): Flow? = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnNullableFlow") + public fun returnNullableFlowNative(): NativeFlow? = + returnNullableFlow()?.asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableFlowAndValueFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + fun returnNullableFlowAndValue(): Flow? = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnNullableFlowAndValue") + public fun returnNullableFlowAndValueNative(): NativeFlow? = + returnNullableFlowAndValue()?.asNativeFlow(null) + """.trimIndent()) + + @Test + fun stateFlowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @NativeCoroutines + fun returnStateFlowValue(): StateFlow = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnStateFlowValue") + public fun returnStateFlowValueNative(): NativeFlow = + returnStateFlowValue().asNativeFlow(null) + """.trimIndent()) + + @Test + fun suspendFlowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + suspend fun returnSuspendFlowValue(): Flow = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendFlowValue") + public fun returnSuspendFlowValueNative(): NativeSuspend> = nativeSuspend(null) { + returnSuspendFlowValue().asNativeFlow(null) } + """.trimIndent()) + + @Test + fun suspendNullableFlowFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + suspend fun returnSuspendFlowValue(): Flow? = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendFlowValue") + public fun returnSuspendFlowValueNative(): NativeSuspend?> = nativeSuspend(null) + { returnSuspendFlowValue()?.asNativeFlow(null) } + """.trimIndent()) + + @Test + fun genericFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @NativeCoroutines + suspend fun returnGenericSuspendValue(): T = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.native.ObjCName + + @ObjCName(name = "returnGenericSuspendValue") + public fun returnGenericSuspendValueNative(): NativeSuspend = nativeSuspend(null) { + returnGenericSuspendValue() } + """.trimIndent()) + + @Test + fun genericClassFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + class MyClass { + @NativeCoroutines + suspend fun returnClassGenericSuspendValue(): T = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.native.ObjCName + + @ObjCName(name = "returnClassGenericSuspendValue") + public fun MyClass.returnClassGenericSuspendValueNative(): NativeSuspend = + nativeSuspend(null) { returnClassGenericSuspendValue() } + """.trimIndent()) + + @Test + fun genericClassGenericFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + class MyClass { + @NativeCoroutines + suspend fun returnGenericSuspendValue(input: T): R = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.native.ObjCName + + @ObjCName(name = "returnGenericSuspendValue") + public fun MyClass.returnGenericSuspendValueNative(input: T): NativeSuspend = + nativeSuspend(null) { returnGenericSuspendValue(input) } + """.trimIndent()) + + @Test + fun kdocFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + /** + * KDoc for [returnSuspendValue] + */ + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + /** + * KDoc for [returnSuspendValue] + */ + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(): NativeSuspend = nativeSuspend(null) { + returnSuspendValue() } + """.trimIndent()) + + @Test + fun protectedOpenClassFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + open class MyClass { + @NativeCoroutines + protected suspend fun returnSuspendValue(): String = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun MyClass.returnSuspendValueNative(): NativeSuspend = nativeSuspend(null) { + returnSuspendValue() } + """.trimIndent()) + + @Test + fun extensionFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @NativeCoroutines + suspend fun String.returnReceiverValue(): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnReceiverValue") + public fun String.returnReceiverValueNative(): NativeSuspend = nativeSuspend(null) { + returnReceiverValue() } + """.trimIndent()) + + @Test + fun classExtensionFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + class MyClass { + @NativeCoroutines + suspend fun String.returnReceiverValue(): String = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + import kotlin.run + + @ObjCName(name = "returnReceiverValue") + public fun MyClass.returnReceiverValueNative(`receiver`: String): NativeSuspend = + nativeSuspend(null) { run { `receiver`.returnReceiverValue() } } + """.trimIndent()) + + @Test + fun parameterFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @NativeCoroutines + suspend fun returnSuspendValue(value: String): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(`value`: String): NativeSuspend = nativeSuspend(null) { + returnSuspendValue(`value`) } + """.trimIndent()) + + @Test + fun implicitTypeFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @NativeCoroutines + suspend fun returnSuspendValue(value: String) = value + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(`value`: String): NativeSuspend = nativeSuspend(null) { + returnSuspendValue(`value`) } + """.trimIndent()) + + @Test + fun implicitFlowTypeFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.flow + + @NativeCoroutines + fun returnFlowValue(value: String) = flow { emit(value) } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnFlowValue") + public fun returnFlowValueNative(`value`: String): NativeFlow = + returnFlowValue(`value`).asNativeFlow(null) + """.trimIndent()) + + @Test + fun annotatedFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @Deprecated("it's old") + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.Deprecated + import kotlin.DeprecationLevel + import kotlin.ReplaceWith + import kotlin.String + import kotlin.native.ObjCName + + @Deprecated( + message = "it's old", + replaceWith = ReplaceWith(expression = "", imports = arrayOf()), + level = DeprecationLevel.WARNING, + ) + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(): NativeSuspend = nativeSuspend(null) { + returnSuspendValue() } + """.trimIndent()) + + /** + * We can't test this since [Throws] is a typealias in Kotlin/JVM + * which is where our KSP tests are currently running. + */ + @Test + @Ignore + fun throwsAnnotation() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @Throws + @NativeCoroutines + suspend fun returnSuspendValue(): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(): NativeSuspend = nativeSuspend(null) { + returnSuspendValue() } + """.trimIndent()) + + @Test + fun varargParameterFunction() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + + @NativeCoroutines + suspend fun returnSuspendValue(vararg values: String): String = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeSuspend + import com.rickclephas.kmp.nativecoroutines.nativeSuspend + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "returnSuspendValue") + public fun returnSuspendValueNative(vararg values: String): NativeSuspend = + nativeSuspend(null) { returnSuspendValue(*values) } + """.trimIndent()) +} diff --git a/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesPropertySpecsTests.kt b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesPropertySpecsTests.kt new file mode 100644 index 00000000..882af7ed --- /dev/null +++ b/kmp-nativecoroutines-ksp/src/test/kotlin/com/rickclephas/kmp/nativecoroutines/ksp/NativeCoroutinesPropertySpecsTests.kt @@ -0,0 +1,773 @@ +package com.rickclephas.kmp.nativecoroutines.ksp + +import org.junit.Ignore +import org.junit.Test + +class NativeCoroutinesPropertySpecsTests: CompilationTests() { + + @Test + fun globalFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + val globalFlow: Flow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "globalFlow") + public val globalFlowNative: NativeFlow + get() = globalFlow.asNativeFlow(null) + """.trimIndent()) + + @Test + fun globalSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + @NativeCoroutines + val globalSharedFlow: SharedFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "globalSharedFlow") + public val globalSharedFlowNative: NativeFlow + get() = globalSharedFlow.asNativeFlow(null) + + public val globalSharedFlowReplayCache: List + get() = globalSharedFlow.replayCache + """.trimIndent()) + + @Test + fun globalStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @NativeCoroutines + val globalStateFlow: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "globalStateFlow") + public val globalStateFlowNative: NativeFlow + get() = globalStateFlow.asNativeFlow(null) + + public val globalStateFlowValue: String + get() = globalStateFlow.value + """.trimIndent()) + + @Test + fun globalMutableStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.MutableStateFlow + + @NativeCoroutines + val globalMutableStateFlow: MutableStateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "globalMutableStateFlow") + public val globalMutableStateFlowNative: NativeFlow + get() = globalMutableStateFlow.asNativeFlow(null) + + public var globalMutableStateFlowValue: String + get() = globalMutableStateFlow.value + set(`value`) { + globalMutableStateFlow.value = value + } + """.trimIndent()) + + @Test + fun globalCustomFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.AbstractFlow + + abstract class CustomFlow: AbstractFlow() + + @NativeCoroutines + val globalCustomFlow: CustomFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "globalCustomFlow") + public val globalCustomFlowNative: NativeFlow + get() = globalCustomFlow.asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableCustomFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.AbstractFlow + + abstract class CustomFlow: AbstractFlow() + + @NativeCoroutines + val nullableCustomFlow: CustomFlow? get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableCustomFlow") + public val nullableCustomFlowNative: NativeFlow? + get() = nullableCustomFlow?.asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableCustomFlowValueProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.AbstractFlow + + abstract class CustomFlow: AbstractFlow() + + @NativeCoroutines + val nullableCustomFlowValue: CustomFlow get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableCustomFlowValue") + public val nullableCustomFlowValueNative: NativeFlow + get() = nullableCustomFlowValue.asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableFlowValueProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + val nullableFlowValue: Flow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableFlowValue") + public val nullableFlowValueNative: NativeFlow + get() = nullableFlowValue.asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableSharedFlowValueProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + @NativeCoroutines + val nullableSharedFlowValue: SharedFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "nullableSharedFlowValue") + public val nullableSharedFlowValueNative: NativeFlow + get() = nullableSharedFlowValue.asNativeFlow(null) + + public val nullableSharedFlowValueReplayCache: List + get() = nullableSharedFlowValue.replayCache + """.trimIndent()) + + @Test + fun nullableStateFlowValueProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @NativeCoroutines + val nullableStateFlowValue: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableStateFlowValue") + public val nullableStateFlowValueNative: NativeFlow + get() = nullableStateFlowValue.asNativeFlow(null) + + public val nullableStateFlowValueValue: String? + get() = nullableStateFlowValue.value + """.trimIndent()) + + @Test + fun nullableFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + val nullableFlow: Flow? get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableFlow") + public val nullableFlowNative: NativeFlow? + get() = nullableFlow?.asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + @NativeCoroutines + val nullableSharedFlow: SharedFlow? get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "nullableSharedFlow") + public val nullableSharedFlowNative: NativeFlow? + get() = nullableSharedFlow?.asNativeFlow(null) + + public val nullableSharedFlowReplayCache: List? + get() = nullableSharedFlow?.replayCache + """.trimIndent()) + + @Test + fun nullableStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @NativeCoroutines + val nullableStateFlow: StateFlow? get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableStateFlow") + public val nullableStateFlowNative: NativeFlow? + get() = nullableStateFlow?.asNativeFlow(null) + + public val nullableStateFlowValue: String? + get() = nullableStateFlow?.value + """.trimIndent()) + + @Test + fun nullableFlowAndValueProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.Flow + + @NativeCoroutines + val nullableFlowAndValue: Flow? get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableFlowAndValue") + public val nullableFlowAndValueNative: NativeFlow? + get() = nullableFlowAndValue?.asNativeFlow(null) + """.trimIndent()) + + @Test + fun nullableSharedFlowAndValueProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + @NativeCoroutines + val nullableSharedFlowAndValue: SharedFlow? get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "nullableSharedFlowAndValue") + public val nullableSharedFlowAndValueNative: NativeFlow? + get() = nullableSharedFlowAndValue?.asNativeFlow(null) + + public val nullableSharedFlowAndValueReplayCache: List? + get() = nullableSharedFlowAndValue?.replayCache + """.trimIndent()) + + @Test + fun nullableStateFlowAndValueProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @NativeCoroutines + val nullableStateFlowAndValue: StateFlow? get() = null + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "nullableStateFlowAndValue") + public val nullableStateFlowAndValueNative: NativeFlow? + get() = nullableStateFlowAndValue?.asNativeFlow(null) + + public val nullableStateFlowAndValueValue: String? + get() = nullableStateFlowAndValue?.value + """.trimIndent()) + + @Test + fun genericSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + class MyClass + + @NativeCoroutines + val MyClass.genericSharedFlow: SharedFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "genericSharedFlow") + public val MyClass.genericSharedFlowNative: NativeFlow + get() = genericSharedFlow.asNativeFlow(null) + + public val MyClass.genericSharedFlowReplayCache: List + get() = genericSharedFlow.replayCache + """.trimIndent()) + + @Test + fun genericStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + class MyClass + + @NativeCoroutines + val MyClass.genericStateFlow: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.native.ObjCName + + @ObjCName(name = "genericStateFlow") + public val MyClass.genericStateFlowNative: NativeFlow + get() = genericStateFlow.asNativeFlow(null) + + public val MyClass.genericStateFlowValue: T + get() = genericStateFlow.value + """.trimIndent()) + + @Test + fun genericClassSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + class MyClass { + @NativeCoroutines + val genericSharedFlow: SharedFlow get() = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "genericSharedFlow") + public val MyClass.genericSharedFlowNative: NativeFlow + get() = genericSharedFlow.asNativeFlow(null) + + public val MyClass.genericSharedFlowReplayCache: List + get() = genericSharedFlow.replayCache + """.trimIndent()) + + @Test + fun genericClassStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + class MyClass { + @NativeCoroutines + val genericStateFlow: StateFlow get() = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.native.ObjCName + + @ObjCName(name = "genericStateFlow") + public val MyClass.genericStateFlowNative: NativeFlow + get() = genericStateFlow.asNativeFlow(null) + + public val MyClass.genericStateFlowValue: T + get() = genericStateFlow.value + """.trimIndent()) + + @Test + fun kdocSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + /** + * KDoc for [kdocSharedFlow] + */ + @NativeCoroutines + val kdocSharedFlow: SharedFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + /** + * KDoc for [kdocSharedFlow] + */ + @ObjCName(name = "kdocSharedFlow") + public val kdocSharedFlowNative: NativeFlow + get() = kdocSharedFlow.asNativeFlow(null) + + /** + * KDoc for [kdocSharedFlow] + */ + public val kdocSharedFlowReplayCache: List + get() = kdocSharedFlow.replayCache + """.trimIndent()) + + @Test + fun kdocStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + /** + * KDoc for [kdocStateFlow] + */ + @NativeCoroutines + val kdocStateFlow: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + /** + * KDoc for [kdocStateFlow] + */ + @ObjCName(name = "kdocStateFlow") + public val kdocStateFlowNative: NativeFlow + get() = kdocStateFlow.asNativeFlow(null) + + /** + * KDoc for [kdocStateFlow] + */ + public val kdocStateFlowValue: String + get() = kdocStateFlow.value + """.trimIndent()) + + @Test + fun protectedOpenClassSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + open class MyClass { + @NativeCoroutines + protected val sharedFlow: SharedFlow get() = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "sharedFlow") + public val MyClass.sharedFlowNative: NativeFlow + get() = sharedFlow.asNativeFlow(null) + + public val MyClass.sharedFlowReplayCache: List + get() = sharedFlow.replayCache + """.trimIndent()) + + @Test + fun protectedOpenClassStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + open class MyClass { + @NativeCoroutines + protected val stateFlow: StateFlow get() = TODO() + } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "stateFlow") + public val MyClass.stateFlowNative: NativeFlow + get() = stateFlow.asNativeFlow(null) + + public val MyClass.stateFlowValue: String + get() = stateFlow.value + """.trimIndent()) + + @Test + fun extensionSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + @NativeCoroutines + val String.sharedFlow: SharedFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "sharedFlow") + public val String.sharedFlowNative: NativeFlow + get() = sharedFlow.asNativeFlow(null) + + public val String.sharedFlowReplayCache: List + get() = sharedFlow.replayCache + """.trimIndent()) + + @Test + fun extensionStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @NativeCoroutines + val String.stateFlow: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "stateFlow") + public val String.stateFlowNative: NativeFlow + get() = stateFlow.asNativeFlow(null) + + public val String.stateFlowValue: String + get() = stateFlow.value + """.trimIndent()) + + @Test + fun implicitTypeProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.flow + + @NativeCoroutines + val myFlow get() = flow { emit("") } + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "myFlow") + public val myFlowNative: NativeFlow + get() = myFlow.asNativeFlow(null) + """.trimIndent()) + + @Test + fun annotatedSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + @Deprecated("it's old") + @get:ExperimentalStdlibApi + @NativeCoroutines + val sharedFlow: SharedFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.Deprecated + import kotlin.DeprecationLevel + import kotlin.ExperimentalStdlibApi + import kotlin.ReplaceWith + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @Deprecated( + message = "it's old", + replaceWith = ReplaceWith(expression = "", imports = arrayOf()), + level = DeprecationLevel.WARNING, + ) + @ObjCName(name = "sharedFlow") + public val sharedFlowNative: NativeFlow + @get:ExperimentalStdlibApi + get() = sharedFlow.asNativeFlow(null) + + @Deprecated( + message = "it's old", + replaceWith = ReplaceWith(expression = "", imports = arrayOf()), + level = DeprecationLevel.WARNING, + ) + public val sharedFlowReplayCache: List + @get:ExperimentalStdlibApi + get() = sharedFlow.replayCache + """.trimIndent()) + + @Test + fun annotatedStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @Deprecated("it's old") + @get:ExperimentalStdlibApi + @NativeCoroutines + val stateFlow: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.Deprecated + import kotlin.DeprecationLevel + import kotlin.ExperimentalStdlibApi + import kotlin.ReplaceWith + import kotlin.String + import kotlin.native.ObjCName + + @Deprecated( + message = "it's old", + replaceWith = ReplaceWith(expression = "", imports = arrayOf()), + level = DeprecationLevel.WARNING, + ) + @ObjCName(name = "stateFlow") + public val stateFlowNative: NativeFlow + @get:ExperimentalStdlibApi + get() = stateFlow.asNativeFlow(null) + + @Deprecated( + message = "it's old", + replaceWith = ReplaceWith(expression = "", imports = arrayOf()), + level = DeprecationLevel.WARNING, + ) + public val stateFlowValue: String + @get:ExperimentalStdlibApi + get() = stateFlow.value + """.trimIndent()) + + /** + * We can't test this since [Throws] is a typealias in Kotlin/JVM + * which is where our KSP tests are currently running. + */ + @Test + @Ignore + fun throwsAnnotationSharedFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.SharedFlow + + @get:Throws + @NativeCoroutines + val sharedFlow: SharedFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.collections.List + import kotlin.native.ObjCName + + @ObjCName(name = "sharedFlow") + public val sharedFlowNative: NativeFlow + get() = sharedFlow.asNativeFlow(null) + + public val sharedFlowReplayCache: List + get() = sharedFlow.replayCache + """.trimIndent()) + + /** + * We can't test this since [Throws] is a typealias in Kotlin/JVM + * which is where our KSP tests are currently running. + */ + @Test + @Ignore + fun throwsAnnotationStateFlowProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutines + import kotlinx.coroutines.flow.StateFlow + + @get:Throws + @NativeCoroutines + val stateFlow: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + @ObjCName(name = "stateFlow") + public val stateFlowNative: NativeFlow + get() = stateFlow.asNativeFlow(null) + + public val stateFlowValue: String + get() = stateFlow.value + """.trimIndent()) + + @Test + fun globalStateProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState + import kotlinx.coroutines.flow.StateFlow + + @NativeCoroutinesState + val globalState: StateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + public val globalStateFlow: NativeFlow + get() = globalState.asNativeFlow(null) + + @ObjCName(name = "globalState") + public val globalStateValue: String + get() = globalState.value + """.trimIndent()) + + @Test + fun globalMutableStateProperty() = runKspTest(""" + import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState + import kotlinx.coroutines.flow.MutableStateFlow + + @NativeCoroutinesState + val globalMutableState: MutableStateFlow get() = TODO() + """.trimIndent(), """ + import com.rickclephas.kmp.nativecoroutines.NativeFlow + import com.rickclephas.kmp.nativecoroutines.asNativeFlow + import kotlin.String + import kotlin.native.ObjCName + + public val globalMutableStateFlow: NativeFlow + get() = globalMutableState.asNativeFlow(null) + + @ObjCName(name = "globalMutableState") + public var globalMutableStateValue: String + get() = globalMutableState.value + set(`value`) { + globalMutableState.value = value + } + """.trimIndent()) +} diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 5a9e61e8..16ef453f 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,6 +2,27 @@ # yarn lockfile v1 +"@babel/code-frame@^7.10.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -12,6 +33,88 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@rollup/plugin-commonjs@^21.0.1": + version "21.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.1.0.tgz#45576d7b47609af2db87f55a6d4b46e44fc3a553" + integrity sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + +"@rollup/plugin-node-resolve@^13.1.3": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c" + integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + deepmerge "^4.2.2" + is-builtin-module "^3.1.0" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/plugin-typescript@^8.3.0": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz#7ea11599a15b0a30fa7ea69ce3b791d41b862515" + integrity sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + resolve "^1.17.0" + +"@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + "@socket.io/component-emitter@~3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" @@ -48,6 +151,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + "@types/json-schema@*", "@types/json-schema@^7.0.8": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -58,6 +166,18 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.24.tgz#20ba1bf69c1b4ab405c7a01e950c4f446b05029f" integrity sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g== +"@types/node@^12.12.14": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" @@ -264,6 +384,13 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -284,6 +411,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -360,6 +492,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -383,6 +520,15 @@ caniuse-lite@^1.0.30001317: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd" integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -429,6 +575,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -436,6 +589,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -456,6 +614,11 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -527,6 +690,16 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +deepmerge@^4.2.2: + version "4.3.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" + integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -636,6 +809,11 @@ escape-string-regexp@4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -661,6 +839,16 @@ estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -742,7 +930,7 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== -format-util@1.0.5: +format-util@1.0.5, format-util@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== @@ -809,11 +997,28 @@ glob@7.2.0, glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -903,6 +1108,13 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-builtin-module@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" @@ -910,6 +1122,18 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -927,6 +1151,11 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -944,11 +1173,25 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" @@ -964,6 +1207,15 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -973,6 +1225,11 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1107,6 +1364,13 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.0.9" +magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -1141,7 +1405,7 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.4: +minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1322,7 +1586,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -1409,6 +1673,15 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve@^1.17.0, resolve@^1.19.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.9.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" @@ -1430,6 +1703,31 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rollup-plugin-sourcemaps@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed" + integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw== + dependencies: + "@rollup/pluginutils" "^3.0.9" + source-map-resolve "^0.6.0" + +rollup-plugin-terser@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup@^2.68.0: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -1456,6 +1754,13 @@ serialize-javascript@6.0.0, serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -1528,6 +1833,14 @@ source-map-loader@4.0.0: iconv-lite "^0.6.3" source-map-js "^1.0.2" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@0.5.21, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -1546,6 +1859,11 @@ source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -1593,7 +1911,14 @@ supports-color@8.1.1, supports-color@^8.0.0: dependencies: has-flag "^4.0.0" -supports-color@^7.1.0: +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -1621,6 +1946,16 @@ terser-webpack-plugin@^5.1.3: source-map "^0.6.1" terser "^5.7.2" +terser@^5.0.0: + version "5.16.5" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.5.tgz#1c285ca0655f467f92af1bbab46ab72d1cb08e5a" + integrity sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + terser@^5.7.2: version "5.12.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c" @@ -1650,6 +1985,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +tslib@^2.3.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -1658,6 +1998,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typescript@4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +typescript@^3.7.2: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + ua-parser-js@^0.7.30: version "0.7.31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" diff --git a/sample/Async/AsyncFunctionIntegrationTests.swift b/sample/Async/AsyncFunctionIntegrationTests.swift index d724c4f1..43d54321 100644 --- a/sample/Async/AsyncFunctionIntegrationTests.swift +++ b/sample/Async/AsyncFunctionIntegrationTests.swift @@ -14,14 +14,14 @@ class AsyncFunctionIntegrationTests: XCTestCase { func testValueReceived() async throws { let integrationTests = SuspendIntegrationTests() let sendValue = randomInt() - let value = try await asyncFunction(for: integrationTests.returnValueNative(value: sendValue, delay: 1000)) + let value = try await asyncFunction(for: integrationTests.returnValue(value: sendValue, delay: 1000)) XCTAssertEqual(value.int32Value, sendValue, "Received incorrect value") await assertJobCompleted(integrationTests) } func testNilValueReceived() async throws { let integrationTests = SuspendIntegrationTests() - let value = try await asyncFunction(for: integrationTests.returnNullNative(delay: 1000)) + let value = try await asyncFunction(for: integrationTests.returnNull(delay: 1000)) XCTAssertNil(value, "Value should be nil") await assertJobCompleted(integrationTests) } @@ -30,7 +30,7 @@ class AsyncFunctionIntegrationTests: XCTestCase { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() do { - _ = try await asyncFunction(for: integrationTests.throwExceptionNative(message: sendMessage, delay: 1000)) + _ = try await asyncFunction(for: integrationTests.throwException(message: sendMessage, delay: 1000)) XCTFail("Function should complete with an error") } catch { let error = error as NSError @@ -45,7 +45,7 @@ class AsyncFunctionIntegrationTests: XCTestCase { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() do { - _ = try await asyncFunction(for: integrationTests.throwErrorNative(message: sendMessage, delay: 1000)) + _ = try await asyncFunction(for: integrationTests.throwError(message: sendMessage, delay: 1000)) XCTFail("Function should complete with an error") } catch { let error = error as NSError @@ -59,7 +59,7 @@ class AsyncFunctionIntegrationTests: XCTestCase { func testCancellation() async { let integrationTests = SuspendIntegrationTests() let handle = Task { - return try await asyncFunction(for: integrationTests.returnFromCallbackNative(delay: 3000) { + return try await asyncFunction(for: integrationTests.returnFromCallback(delay: 3000) { XCTFail("Callback shouldn't be invoked") return KotlinInt(int: 1) }) @@ -71,9 +71,7 @@ class AsyncFunctionIntegrationTests: XCTestCase { let result = await handle.result await assertJobCompleted(integrationTests) if case let .failure(error) = result { - let error = error as NSError - let exception = error.userInfo["KotlinException"] - XCTAssertTrue(exception is KotlinCancellationException, "Error should be a KotlinCancellationException") + XCTAssertTrue(error is CancellationError, "Error should be a CancellationError") } else { XCTFail("Function should fail with an error") } diff --git a/sample/Async/AsyncResultIntegrationTests.swift b/sample/Async/AsyncResultIntegrationTests.swift index abf5785b..117ce57b 100644 --- a/sample/Async/AsyncResultIntegrationTests.swift +++ b/sample/Async/AsyncResultIntegrationTests.swift @@ -14,7 +14,7 @@ class AsyncResultIntegrationTests: XCTestCase { func testValueReceived() async { let integrationTests = SuspendIntegrationTests() let sendValue = randomInt() - let result = await asyncResult(for: integrationTests.returnValueNative(value: sendValue, delay: 1000)) + let result = await asyncResult(for: integrationTests.returnValue(value: sendValue, delay: 1000)) guard case let .success(value) = result else { XCTFail("Function should complete without an error") return @@ -25,7 +25,7 @@ class AsyncResultIntegrationTests: XCTestCase { func testNilValueReceived() async { let integrationTests = SuspendIntegrationTests() - let result = await asyncResult(for: integrationTests.returnNullNative(delay: 1000)) + let result = await asyncResult(for: integrationTests.returnNull(delay: 1000)) guard case let .success(value) = result else { XCTFail("Function should complete without an error") return @@ -37,7 +37,7 @@ class AsyncResultIntegrationTests: XCTestCase { func testExceptionReceived() async { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() - let result = await asyncResult(for: integrationTests.throwExceptionNative(message: sendMessage, delay: 1000)) + let result = await asyncResult(for: integrationTests.throwException(message: sendMessage, delay: 1000)) guard case let .failure(error) = result else { XCTFail("Function should complete with an error") return @@ -52,7 +52,7 @@ class AsyncResultIntegrationTests: XCTestCase { func testErrorReceived() async { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() - let result = await asyncResult(for: integrationTests.throwErrorNative(message: sendMessage, delay: 1000)) + let result = await asyncResult(for: integrationTests.throwError(message: sendMessage, delay: 1000)) guard case let .failure(error) = result else { XCTFail("Function should complete with an error") return @@ -67,7 +67,7 @@ class AsyncResultIntegrationTests: XCTestCase { func testCancellation() async { let integrationTests = SuspendIntegrationTests() let handle = Task { - return await asyncResult(for: integrationTests.returnFromCallbackNative(delay: 3000) { + return await asyncResult(for: integrationTests.returnFromCallback(delay: 3000) { XCTFail("Callback shouldn't be invoked") return KotlinInt(int: 1) }) @@ -83,9 +83,7 @@ class AsyncResultIntegrationTests: XCTestCase { return } if case let .failure(error) = result { - let error = error as NSError - let exception = error.userInfo["KotlinException"] - XCTAssertTrue(exception is KotlinCancellationException, "Error should be a KotlinCancellationException") + XCTAssertTrue(error is CancellationError, "Error should be a CancellationError") } else { XCTFail("Function should fail with an error") } diff --git a/sample/Async/AsyncStreamIntegrationTests.swift b/sample/Async/AsyncSequenceIntegrationTests.swift similarity index 65% rename from sample/Async/AsyncStreamIntegrationTests.swift rename to sample/Async/AsyncSequenceIntegrationTests.swift index 4bc95a1e..04177f40 100644 --- a/sample/Async/AsyncStreamIntegrationTests.swift +++ b/sample/Async/AsyncSequenceIntegrationTests.swift @@ -1,23 +1,23 @@ // -// AsyncStreamIntegrationTests.swift -// AsyncStreamIntegrationTests +// AsyncSequenceIntegrationTests.swift +// AsyncSequenceIntegrationTests // -// Created by Rick Clephas on 16/07/2021. +// Created by Rick Clephas on 06/03/2022. // import XCTest import KMPNativeCoroutinesAsync import NativeCoroutinesSampleShared -class AsyncStreamIntegrationTests: XCTestCase { +class AsyncSequenceIntegrationTests: XCTestCase { func testValuesReceived() async { let integrationTests = FlowIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) - let stream = asyncStream(for: integrationTests.getFlowNative(count: sendValueCount, delay: 100)) + let sequence = asyncSequence(for: integrationTests.getFlow(count: sendValueCount, delay: 100)) do { var receivedValueCount: Int32 = 0 - for try await value in stream { + for try await value in sequence { if receivedValueCount + 1 < sendValueCount { // Depending on the timing the job might already have completed for the last value, // so we won't check this for the last value but only for earlier values @@ -28,7 +28,27 @@ class AsyncStreamIntegrationTests: XCTestCase { } XCTAssertEqual(receivedValueCount, sendValueCount, "Should have received all values") } catch { - XCTFail("Stream should complete without an error") + XCTFail("Sequence should complete without an error") + } + await assertJobCompleted(integrationTests) + } + + func testValueBackPressure() async { + let integrationTests = FlowIntegrationTests() + let sendValueCount: Int32 = 10 + let sequence = asyncSequence(for: integrationTests.getFlow(count: sendValueCount, delay: 100)) + do { + var receivedValueCount: Int32 = 0 + for try await _ in sequence { + let emittedCount = integrationTests.emittedCount + // Note the AsyncSequence buffers at most a single item + XCTAssert(emittedCount == receivedValueCount || emittedCount == receivedValueCount + 1, "Back pressure isn't applied") + delay(0.2) + receivedValueCount += 1 + } + XCTAssertEqual(receivedValueCount, sendValueCount, "Should have received all values") + } catch { + XCTFail("Sequence should complete without an error") } await assertJobCompleted(integrationTests) } @@ -37,10 +57,10 @@ class AsyncStreamIntegrationTests: XCTestCase { let integrationTests = FlowIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) let nullValueIndex = randomInt(min: 0, max: sendValueCount - 1) - let stream = asyncStream(for: integrationTests.getFlowWithNullNative(count: sendValueCount, nullIndex: nullValueIndex, delay: 100)) + let sequence = asyncSequence(for: integrationTests.getFlowWithNull(count: sendValueCount, nullIndex: nullValueIndex, delay: 100)) do { var receivedValueCount: Int32 = 0 - for try await value in stream { + for try await value in sequence { if receivedValueCount + 1 < sendValueCount { // Depending on the timing the job might already have completed for the last value, // so we won't check this for the last value but only for earlier values @@ -55,7 +75,7 @@ class AsyncStreamIntegrationTests: XCTestCase { } XCTAssertEqual(receivedValueCount, sendValueCount, "Should have received all values") } catch { - XCTFail("Stream should complete without an error") + XCTFail("Sequence should complete without an error") } await assertJobCompleted(integrationTests) } @@ -65,14 +85,14 @@ class AsyncStreamIntegrationTests: XCTestCase { let sendValueCount = randomInt(min: 5, max: 20) let exceptionIndex = randomInt(min: 1, max: sendValueCount - 1) let sendMessage = randomString() - let stream = asyncStream(for: integrationTests.getFlowWithExceptionNative(count: sendValueCount, exceptionIndex: exceptionIndex, message: sendMessage, delay: 100)) + let sequence = asyncSequence(for: integrationTests.getFlowWithException(count: sendValueCount, exceptionIndex: exceptionIndex, message: sendMessage, delay: 100)) var receivedValueCount: Int32 = 0 do { - for try await _ in stream { + for try await _ in sequence { XCTAssertEqual(integrationTests.uncompletedJobCount, 1, "There should be 1 uncompleted job") receivedValueCount += 1 } - XCTFail("Stream should fail with an error") + XCTFail("Sequence should fail with an error") } catch { let error = error as NSError XCTAssertEqual(error.localizedDescription, sendMessage, "Error has incorrect localizedDescription") @@ -88,14 +108,14 @@ class AsyncStreamIntegrationTests: XCTestCase { let sendValueCount = randomInt(min: 5, max: 20) let errorIndex = randomInt(min: 1, max: sendValueCount - 1) let sendMessage = randomString() - let stream = asyncStream(for: integrationTests.getFlowWithErrorNative(count: sendValueCount, errorIndex: errorIndex, message: sendMessage, delay: 100)) + let sequence = asyncSequence(for: integrationTests.getFlowWithError(count: sendValueCount, errorIndex: errorIndex, message: sendMessage, delay: 100)) var receivedValueCount: Int32 = 0 do { - for try await _ in stream { + for try await _ in sequence { XCTAssertEqual(integrationTests.uncompletedJobCount, 1, "There should be 1 uncompleted job") receivedValueCount += 1 } - XCTFail("Stream should fail with an error") + XCTFail("Sequence should fail with an error") } catch { let error = error as NSError XCTAssertEqual(error.localizedDescription, sendMessage, "Error has incorrect localizedDescription") @@ -108,29 +128,23 @@ class AsyncStreamIntegrationTests: XCTestCase { func testCancellation() async { let integrationTests = FlowIntegrationTests() - var callbackCount = 0 - let stream = asyncStream(for: integrationTests.getFlowWithCallbackNative(count: 5, callbackIndex: 2, delay: 1000) { - callbackCount += 1 - }) - let handle = Task { + let handle = Task { do { - var receivedValueCount: Int32 = 0 - for try await _ in stream { + let sequence = asyncSequence(for: integrationTests.getFlowWithCallback(count: 5, callbackIndex: 2, delay: 1000) { + XCTFail("The callback shouldn't be called") + }) + for try await _ in sequence { XCTAssertEqual(integrationTests.uncompletedJobCount, 1, "There should be 1 uncompleted job") - receivedValueCount += 1 } - return receivedValueCount } catch { - XCTFail("Stream should be cancelled without an error") - return -1 + XCTFail("Sequence should be cancelled without an error") } } DispatchQueue.global().asyncAfter(deadline: .now() + 2) { XCTAssertEqual(integrationTests.activeJobCount, 1, "There should be 1 active job") handle.cancel() } - _ = await handle.result + await handle.value await assertJobCompleted(integrationTests) - XCTAssertEqual(callbackCount, 0, "The callback shouldn't be called") } } diff --git a/sample/Async/ClockAsyncViewModel.swift b/sample/Async/ClockAsyncViewModel.swift index 25e10ad0..0b4b2e77 100644 --- a/sample/Async/ClockAsyncViewModel.swift +++ b/sample/Async/ClockAsyncViewModel.swift @@ -27,14 +27,14 @@ class ClockAsyncViewModel: ClockViewModel { func startMonitoring() { task = Task { [weak self] in - let timeStream = asyncStream(for: clock.timeNative) + let timeSequence = asyncSequence(for: clock.time) .map { [weak self] time -> String in guard let self = self else { return "" } let date = Date(timeIntervalSince1970: time.doubleValue) return self.formatter.string(from: date) } do { - for try await time in timeStream { + for try await time in timeSequence { self?.time = time } } catch { @@ -53,7 +53,7 @@ class ClockAsyncViewModel: ClockViewModel { func updateTime() { // Convert the seconds since EPOCH to a string // in the format "HH:mm:ss" and update the UI - let date = Date(timeIntervalSince1970: TimeInterval(clock.timeNativeValue)) + let date = Date(timeIntervalSince1970: TimeInterval(clock.timeValue)) time = formatter.string(from: date) } } diff --git a/sample/Async/RandomLettersAsyncViewModel.swift b/sample/Async/RandomLettersAsyncViewModel.swift index 86669997..7dc960de 100644 --- a/sample/Async/RandomLettersAsyncViewModel.swift +++ b/sample/Async/RandomLettersAsyncViewModel.swift @@ -22,7 +22,7 @@ class RandomLettersAsyncViewModel: RandomLettersViewModel { isLoading = true result = nil do { - let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLettersNative(throwException: throwException)) + let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLetters(throwException: throwException)) result = .success(letters) } catch { result = .failure(error) diff --git a/sample/Async/SwiftUIAsyncTest.swift b/sample/Async/SwiftUIAsyncTest.swift index 72c64287..02c31c5d 100644 --- a/sample/Async/SwiftUIAsyncTest.swift +++ b/sample/Async/SwiftUIAsyncTest.swift @@ -20,7 +20,7 @@ struct SwiftUIAsyncTest: View { }.refreshable { print("Refreshable started") do { - let result = try await asyncFunction(for: tests.returnValueNative(value: 20, delay: 10000)) + let result = try await asyncFunction(for: tests.returnValue(value: 20, delay: 10000)) print("Refreshable result: \(result)") } catch { print("Refreshable error: \(error)") @@ -28,7 +28,7 @@ struct SwiftUIAsyncTest: View { }.task { print("Task started") do { - let result = try await asyncFunction(for: tests.returnValueNative(value: 2, delay: 10000)) + let result = try await asyncFunction(for: tests.returnValue(value: 2, delay: 10000)) print("Task result: \(result)") } catch { print("Task error: \(error)") diff --git a/sample/Combine/ClockCombineViewModel.swift b/sample/Combine/ClockCombineViewModel.swift index eae08257..72423392 100644 --- a/sample/Combine/ClockCombineViewModel.swift +++ b/sample/Combine/ClockCombineViewModel.swift @@ -27,7 +27,7 @@ class ClockCombineViewModel: ClockViewModel { } func startMonitoring() { - cancellable = createPublisher(for: clock.timeNative) + cancellable = createPublisher(for: clock.time) // Convert the seconds since EPOCH to a string in the format "HH:mm:ss" .map { [weak self] time -> String in guard let self = self else { return "" } @@ -51,7 +51,7 @@ class ClockCombineViewModel: ClockViewModel { func updateTime() { // Convert the seconds since EPOCH to a string // in the format "HH:mm:ss" and update the UI - let date = Date(timeIntervalSince1970: TimeInterval(clock.timeNativeValue)) + let date = Date(timeIntervalSince1970: TimeInterval(clock.timeValue)) time = formatter.string(from: date) } } diff --git a/sample/Combine/CombineFutureIntegrationTests.swift b/sample/Combine/CombineFutureIntegrationTests.swift index 54dd6a89..fb1e94ab 100644 --- a/sample/Combine/CombineFutureIntegrationTests.swift +++ b/sample/Combine/CombineFutureIntegrationTests.swift @@ -14,7 +14,7 @@ class CombineFutureIntegrationTests: XCTestCase { func testValueReceived() { let integrationTests = SuspendIntegrationTests() let sendValue = randomInt() - let future = createFuture(for: integrationTests.returnValueNative(value: sendValue, delay: 1000)) + let future = createFuture(for: integrationTests.returnValue(value: sendValue, delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let completionExpectation = expectation(description: "Waiting for completion") let cancellable = future.sink { completion in @@ -35,7 +35,7 @@ class CombineFutureIntegrationTests: XCTestCase { func testNilValueReceived() { let integrationTests = SuspendIntegrationTests() - let future = createFuture(for: integrationTests.returnNullNative(delay: 1000)) + let future = createFuture(for: integrationTests.returnNull(delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let completionExpectation = expectation(description: "Waiting for completion") let cancellable = future.sink { completion in @@ -57,7 +57,7 @@ class CombineFutureIntegrationTests: XCTestCase { func testExceptionReceived() { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() - let future = createFuture(for: integrationTests.throwExceptionNative(message: sendMessage, delay: 1000)) + let future = createFuture(for: integrationTests.throwException(message: sendMessage, delay: 1000)) let valueExpectation = expectation(description: "Waiting for no value") valueExpectation.isInverted = true let completionExpectation = expectation(description: "Waiting for completion") @@ -83,7 +83,7 @@ class CombineFutureIntegrationTests: XCTestCase { func testErrorReceived() { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() - let future = createFuture(for: integrationTests.throwErrorNative(message: sendMessage, delay: 1000)) + let future = createFuture(for: integrationTests.throwError(message: sendMessage, delay: 1000)) let valueExpectation = expectation(description: "Waiting for no value") valueExpectation.isInverted = true let completionExpectation = expectation(description: "Waiting for completion") @@ -108,7 +108,7 @@ class CombineFutureIntegrationTests: XCTestCase { func testNotOnMainThread() { let integrationTests = SuspendIntegrationTests() - let future = createFuture(for: integrationTests.returnValueNative(value: 1, delay: 1000)) + let future = createFuture(for: integrationTests.returnValue(value: 1, delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let completionExpectation = expectation(description: "Waiting for completion") XCTAssertTrue(Thread.isMainThread, "Test should run on the main thread") @@ -127,7 +127,7 @@ class CombineFutureIntegrationTests: XCTestCase { let integrationTests = SuspendIntegrationTests() let callbackExpectation = expectation(description: "Waiting for callback not to get called") callbackExpectation.isInverted = true - let future = createFuture(for: integrationTests.returnFromCallbackNative(delay: 3000) { + let future = createFuture(for: integrationTests.returnFromCallback(delay: 3000) { callbackExpectation.fulfill() return KotlinInt(int: 1) }) @@ -152,7 +152,7 @@ class CombineFutureIntegrationTests: XCTestCase { func testValuesReceived() { let integrationTests = SuspendIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) - let publisher = createPublisher(for: integrationTests.getFlowNative(count: sendValueCount, delay: 100)) + let publisher = createPublisher(for: integrationTests.getFlow(count: sendValueCount, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(sendValueCount) let completionExpectation = expectation(description: "Waiting for completion") diff --git a/sample/Combine/CombinePublisherIntegrationTests.swift b/sample/Combine/CombinePublisherIntegrationTests.swift index 9c0997c1..55fd7d38 100644 --- a/sample/Combine/CombinePublisherIntegrationTests.swift +++ b/sample/Combine/CombinePublisherIntegrationTests.swift @@ -14,7 +14,7 @@ class CombinePublisherIntegrationTests: XCTestCase { func testValuesReceived() { let integrationTests = FlowIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) - let publisher = createPublisher(for: integrationTests.getFlowNative(count: sendValueCount, delay: 100)) + let publisher = createPublisher(for: integrationTests.getFlow(count: sendValueCount, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(sendValueCount) let completionExpectation = expectation(description: "Waiting for completion") @@ -40,7 +40,7 @@ class CombinePublisherIntegrationTests: XCTestCase { let integrationTests = FlowIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) let nullValueIndex = randomInt(min: 0, max: sendValueCount - 1) - let publisher = createPublisher(for: integrationTests.getFlowWithNullNative(count: sendValueCount, nullIndex: nullValueIndex, delay: 100)) + let publisher = createPublisher(for: integrationTests.getFlowWithNull(count: sendValueCount, nullIndex: nullValueIndex, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(sendValueCount) let completionExpectation = expectation(description: "Waiting for completion") @@ -71,7 +71,7 @@ class CombinePublisherIntegrationTests: XCTestCase { let sendValueCount = randomInt(min: 5, max: 20) let exceptionIndex = randomInt(min: 1, max: sendValueCount - 1) let sendMessage = randomString() - let publisher = createPublisher(for: integrationTests.getFlowWithExceptionNative(count: sendValueCount, exceptionIndex: exceptionIndex, message: sendMessage, delay: 100)) + let publisher = createPublisher(for: integrationTests.getFlowWithException(count: sendValueCount, exceptionIndex: exceptionIndex, message: sendMessage, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(exceptionIndex) let completionExpectation = expectation(description: "Waiting for completion") @@ -100,7 +100,7 @@ class CombinePublisherIntegrationTests: XCTestCase { let sendValueCount = randomInt(min: 5, max: 20) let errorIndex = randomInt(min: 1, max: sendValueCount - 1) let sendMessage = randomString() - let publisher = createPublisher(for: integrationTests.getFlowWithErrorNative(count: sendValueCount, errorIndex: errorIndex, message: sendMessage, delay: 100)) + let publisher = createPublisher(for: integrationTests.getFlowWithError(count: sendValueCount, errorIndex: errorIndex, message: sendMessage, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(errorIndex) let completionExpectation = expectation(description: "Waiting for completion") @@ -126,7 +126,7 @@ class CombinePublisherIntegrationTests: XCTestCase { func testNotOnMainThread() { let integrationTests = FlowIntegrationTests() - let publisher = createPublisher(for: integrationTests.getFlowNative(count: 1, delay: 1000)) + let publisher = createPublisher(for: integrationTests.getFlow(count: 1, delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let completionExpectation = expectation(description: "Waiting for completion") XCTAssertTrue(Thread.isMainThread, "Test should run on the main thread") @@ -145,7 +145,7 @@ class CombinePublisherIntegrationTests: XCTestCase { let integrationTests = FlowIntegrationTests() let callbackExpectation = expectation(description: "Waiting for callback not to get called") callbackExpectation.isInverted = true - let publisher = createPublisher(for: integrationTests.getFlowWithCallbackNative(count: 5, callbackIndex: 2, delay: 1000) { + let publisher = createPublisher(for: integrationTests.getFlowWithCallback(count: 5, callbackIndex: 2, delay: 1000) { callbackExpectation.fulfill() }) let valuesExpectation = expectation(description: "Waiting for values") diff --git a/sample/Combine/RandomLettersCombineViewModel.swift b/sample/Combine/RandomLettersCombineViewModel.swift index c286a721..e6114f20 100644 --- a/sample/Combine/RandomLettersCombineViewModel.swift +++ b/sample/Combine/RandomLettersCombineViewModel.swift @@ -22,7 +22,7 @@ class RandomLettersCombineViewModel: RandomLettersViewModel { func loadRandomLetters(throwException: Bool) { isLoading = true result = nil - createFuture(for: randomLettersGenerator.getRandomLettersNative(throwException: throwException)) + createFuture(for: randomLettersGenerator.getRandomLetters(throwException: throwException)) // Update the UI on the main thread .receive(on: DispatchQueue.main) .sink { [weak self] completion in diff --git a/sample/IntegrationTests/CompilerIntegrationTests.swift b/sample/IntegrationTests/CompilerIntegrationTests.swift index 2e2d3caf..4f2d443e 100644 --- a/sample/IntegrationTests/CompilerIntegrationTests.swift +++ b/sample/IntegrationTests/CompilerIntegrationTests.swift @@ -17,11 +17,11 @@ class CompilerIntegrationTests: XCTestCase { let integrationTests = IntegrationTests() let valueExpectation = expectation(description: "Waiting for value") let sendValue = NSNumber(value: randomInt()) - _ = integrationTests.returnGenericClassValueNative(value: sendValue)({ value, unit in - XCTAssertEqual(value, sendValue, "Received incorrect value") + _ = integrationTests.returnGenericClassValue(value: sendValue)({ value, unit in + XCTAssertEqual(value as! NSNumber, sendValue, "Received incorrect value") valueExpectation.fulfill() return unit - }, { _, unit in unit }) + }, { _, unit in unit }, { _, unit in unit }) wait(for: [valueExpectation], timeout: 2) } @@ -29,11 +29,11 @@ class CompilerIntegrationTests: XCTestCase { let integrationTests = IntegrationTests() let valueExpectation = expectation(description: "Waiting for value") let sendValue = randomInt() - _ = integrationTests.returnDefaultValueNative(value: sendValue)({ value, unit in + _ = integrationTests.returnDefaultValue(value: sendValue)({ value, unit in XCTAssertEqual(value.int32Value, sendValue, "Received incorrect value") valueExpectation.fulfill() return unit - }, { _, unit in unit }) + }, { _, unit in unit }, { _, unit in unit }) wait(for: [valueExpectation], timeout: 2) } @@ -41,11 +41,11 @@ class CompilerIntegrationTests: XCTestCase { let integrationTests = IntegrationTests() let valueExpectation = expectation(description: "Waiting for value") let sendValue = NSNumber(value: randomInt()) - _ = integrationTests.returnGenericValueNative(value: sendValue)({ value, unit in + _ = integrationTests.returnGenericValue(value: sendValue)({ value, unit in XCTAssertEqual(value as! NSNumber, sendValue, "Received incorrect value") valueExpectation.fulfill() return unit - }, { _, unit in unit }) + }, { _, unit in unit }, { _, unit in unit }) wait(for: [valueExpectation], timeout: 2) } @@ -53,11 +53,11 @@ class CompilerIntegrationTests: XCTestCase { let integrationTests = IntegrationTests() let valueExpectation = expectation(description: "Waiting for value") let sendValue = integrationTests.returnAppendable(value: randomString()) - _ = integrationTests.returnConstrainedGenericValueNative(value: sendValue)({ value, unit in + _ = integrationTests.returnConstrainedGenericValue(value: sendValue)({ value, unit in XCTAssertIdentical(value, sendValue, "Received incorrect value") valueExpectation.fulfill() return unit - }, { _, unit in unit }) + }, { _, unit in unit }, { _, unit in unit }) wait(for: [valueExpectation], timeout: 2) } @@ -65,11 +65,11 @@ class CompilerIntegrationTests: XCTestCase { let integrationTests = IntegrationTests() let valueExpectation = expectation(description: "Waiting for values") let sendValues = [NSNumber(value: randomInt()), NSNumber(value: randomInt())] - _ = integrationTests.returnGenericValuesNative(values: sendValues)({ values, unit in + _ = integrationTests.returnGenericValues(values: sendValues)({ values, unit in XCTAssertEqual(values as! [NSNumber], sendValues, "Received incorrect values") valueExpectation.fulfill() return unit - }, { _, unit in unit }) + }, { _, unit in unit }, { _, unit in unit }) wait(for: [valueExpectation], timeout: 2) } @@ -79,11 +79,14 @@ class CompilerIntegrationTests: XCTestCase { let sendValues = KotlinArray(size: 2) { _ in NSNumber(value: self.randomInt()) } - _ = integrationTests.returnGenericVarargValuesNative(values: sendValues)({ values, unit in - XCTAssertEqual(values, sendValues, "Received incorrect values") + _ = integrationTests.returnGenericVarargValues(values: sendValues)({ values, unit in + XCTAssertEqual(values.size, sendValues.size, "Received incorrect number of value") + for i in 0.. String in guard let self = self else { return "" } @@ -52,7 +52,7 @@ class ClockRxSwiftViewModel: ClockViewModel { func updateTime() { // Convert the seconds since EPOCH to a string // in the format "HH:mm:ss" and update the UI - let date = Date(timeIntervalSince1970: TimeInterval(clock.timeNativeValue)) + let date = Date(timeIntervalSince1970: TimeInterval(clock.timeValue)) time = formatter.string(from: date) } } diff --git a/sample/RxSwift/RandomLettersRxSwiftViewModel.swift b/sample/RxSwift/RandomLettersRxSwiftViewModel.swift index 5132663d..f0d2b518 100644 --- a/sample/RxSwift/RandomLettersRxSwiftViewModel.swift +++ b/sample/RxSwift/RandomLettersRxSwiftViewModel.swift @@ -21,7 +21,7 @@ class RandomLettersRxSwiftViewModel: RandomLettersViewModel { func loadRandomLetters(throwException: Bool) { isLoading = true result = nil - _ = createSingle(for: randomLettersGenerator.getRandomLettersNative(throwException: throwException)) + _ = createSingle(for: randomLettersGenerator.getRandomLetters(throwException: throwException)) // Update the UI on the main thread .observe(on: MainScheduler.instance) .subscribe(onSuccess: { [weak self] word in diff --git a/sample/RxSwift/RxSwiftObservableIntegrationTests.swift b/sample/RxSwift/RxSwiftObservableIntegrationTests.swift index e8367b7a..670c37fa 100644 --- a/sample/RxSwift/RxSwiftObservableIntegrationTests.swift +++ b/sample/RxSwift/RxSwiftObservableIntegrationTests.swift @@ -14,7 +14,7 @@ class RxSwiftObservableIntegrationTests: XCTestCase { func testValuesReceived() { let integrationTests = FlowIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) - let observable = createObservable(for: integrationTests.getFlowNative(count: sendValueCount, delay: 100)) + let observable = createObservable(for: integrationTests.getFlow(count: sendValueCount, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(sendValueCount) let completionExpectation = expectation(description: "Waiting for completion") @@ -42,7 +42,7 @@ class RxSwiftObservableIntegrationTests: XCTestCase { let integrationTests = FlowIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) let nullValueIndex = randomInt(min: 0, max: sendValueCount - 1) - let observable = createObservable(for: integrationTests.getFlowWithNullNative(count: sendValueCount, nullIndex: nullValueIndex, delay: 100)) + let observable = createObservable(for: integrationTests.getFlowWithNull(count: sendValueCount, nullIndex: nullValueIndex, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(sendValueCount) let completionExpectation = expectation(description: "Waiting for completion") @@ -75,7 +75,7 @@ class RxSwiftObservableIntegrationTests: XCTestCase { let sendValueCount = randomInt(min: 5, max: 20) let exceptionIndex = randomInt(min: 1, max: sendValueCount - 1) let sendMessage = randomString() - let observable = createObservable(for: integrationTests.getFlowWithExceptionNative(count: sendValueCount, exceptionIndex: exceptionIndex, message: sendMessage, delay: 100)) + let observable = createObservable(for: integrationTests.getFlowWithException(count: sendValueCount, exceptionIndex: exceptionIndex, message: sendMessage, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(exceptionIndex) let errorExpectation = expectation(description: "Waiting for error") @@ -106,7 +106,7 @@ class RxSwiftObservableIntegrationTests: XCTestCase { let sendValueCount = randomInt(min: 5, max: 20) let errorIndex = randomInt(min: 1, max: sendValueCount - 1) let sendMessage = randomString() - let observable = createObservable(for: integrationTests.getFlowWithErrorNative(count: sendValueCount, errorIndex: errorIndex, message: sendMessage, delay: 100)) + let observable = createObservable(for: integrationTests.getFlowWithError(count: sendValueCount, errorIndex: errorIndex, message: sendMessage, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(errorIndex) let errorExpectation = expectation(description: "Waiting for error") @@ -134,7 +134,7 @@ class RxSwiftObservableIntegrationTests: XCTestCase { func testNotOnMainThread() { let integrationTests = FlowIntegrationTests() - let observable = createObservable(for: integrationTests.getFlowNative(count: 1, delay: 1000)) + let observable = createObservable(for: integrationTests.getFlow(count: 1, delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let completionExpectation = expectation(description: "Waiting for completion") let disposedExpectation = expectation(description: "Waiting for dispose") @@ -157,7 +157,7 @@ class RxSwiftObservableIntegrationTests: XCTestCase { let integrationTests = FlowIntegrationTests() let callbackExpectation = expectation(description: "Waiting for callback not to get called") callbackExpectation.isInverted = true - let observable = createObservable(for: integrationTests.getFlowWithCallbackNative(count: 5, callbackIndex: 2, delay: 1000) { + let observable = createObservable(for: integrationTests.getFlowWithCallback(count: 5, callbackIndex: 2, delay: 1000) { callbackExpectation.fulfill() }) let valuesExpectation = expectation(description: "Waiting for values") diff --git a/sample/RxSwift/RxSwiftSingleIntegrationTests.swift b/sample/RxSwift/RxSwiftSingleIntegrationTests.swift index ca4cc7d1..5f2db57d 100644 --- a/sample/RxSwift/RxSwiftSingleIntegrationTests.swift +++ b/sample/RxSwift/RxSwiftSingleIntegrationTests.swift @@ -14,7 +14,7 @@ class RxSwiftSingleIntegrationTests: XCTestCase { func testValueReceived() { let integrationTests = SuspendIntegrationTests() let sendValue = randomInt() - let single = createSingle(for: integrationTests.returnValueNative(value: sendValue, delay: 1000)) + let single = createSingle(for: integrationTests.returnValue(value: sendValue, delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let disposedExpectation = expectation(description: "Waiting for dispose") let disposable = single.subscribe(onSuccess: { value in @@ -34,7 +34,7 @@ class RxSwiftSingleIntegrationTests: XCTestCase { func testNilValueReceived() { let integrationTests = SuspendIntegrationTests() - let single = createSingle(for: integrationTests.returnNullNative(delay: 1000)) + let single = createSingle(for: integrationTests.returnNull(delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let disposedExpectation = expectation(description: "Waiting for dispose") let disposable = single.subscribe(onSuccess: { value in @@ -55,7 +55,7 @@ class RxSwiftSingleIntegrationTests: XCTestCase { func testExceptionReceived() { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() - let single = createSingle(for: integrationTests.throwExceptionNative(message: sendMessage, delay: 1000)) + let single = createSingle(for: integrationTests.throwException(message: sendMessage, delay: 1000)) let valueExpectation = expectation(description: "Waiting for no value") valueExpectation.isInverted = true let errorExpectation = expectation(description: "Waiting for error") @@ -80,7 +80,7 @@ class RxSwiftSingleIntegrationTests: XCTestCase { func testErrorReceived() { let integrationTests = SuspendIntegrationTests() let sendMessage = randomString() - let single = createSingle(for: integrationTests.throwErrorNative(message: sendMessage, delay: 1000)) + let single = createSingle(for: integrationTests.throwError(message: sendMessage, delay: 1000)) let valueExpectation = expectation(description: "Waiting for no value") valueExpectation.isInverted = true let errorExpectation = expectation(description: "Waiting for error") @@ -104,7 +104,7 @@ class RxSwiftSingleIntegrationTests: XCTestCase { func testNotOnMainThread() { let integrationTests = SuspendIntegrationTests() - let single = createSingle(for: integrationTests.returnValueNative(value: 1, delay: 1000)) + let single = createSingle(for: integrationTests.returnValue(value: 1, delay: 1000)) let valueExpectation = expectation(description: "Waiting for value") let disposedExpectation = expectation(description: "Waiting for dispose") XCTAssertTrue(Thread.isMainThread, "Test should run on the main thread") @@ -123,7 +123,7 @@ class RxSwiftSingleIntegrationTests: XCTestCase { let integrationTests = SuspendIntegrationTests() let callbackExpectation = expectation(description: "Waiting for callback not to get called") callbackExpectation.isInverted = true - let single = createSingle(for: integrationTests.returnFromCallbackNative(delay: 3000) { + let single = createSingle(for: integrationTests.returnFromCallback(delay: 3000) { callbackExpectation.fulfill() return KotlinInt(int: 1) }) @@ -151,7 +151,7 @@ class RxSwiftSingleIntegrationTests: XCTestCase { func testValuesReceived() { let integrationTests = SuspendIntegrationTests() let sendValueCount = randomInt(min: 5, max: 20) - let observable = createObservable(for: integrationTests.getFlowNative(count: sendValueCount, delay: 100)) + let observable = createObservable(for: integrationTests.getFlow(count: sendValueCount, delay: 100)) let valuesExpectation = expectation(description: "Waiting for values") valuesExpectation.expectedFulfillmentCount = Int(sendValueCount) let completionExpectation = expectation(description: "Waiting for completion") diff --git a/sample/settings.gradle.kts b/sample/settings.gradle.kts index 57f0b1b8..931399b4 100644 --- a/sample/settings.gradle.kts +++ b/sample/settings.gradle.kts @@ -12,7 +12,7 @@ include(":shared") includeBuild("..") { dependencySubstitution { - listOf("annotations", "compiler", "compiler-embeddable", "core").forEach { + listOf("annotations", "compiler", "compiler-embeddable", "core", "ksp").forEach { substitute(module("com.rickclephas.kmp:kmp-nativecoroutines-$it")) .using(project(":kmp-nativecoroutines-$it")) } diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index d9025afa..b26e0768 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.kotlin.multiplatform) @Suppress("DSL_SCOPE_VIOLATION") alias(libs.plugins.kotlin.plugin.serialization) + @Suppress("DSL_SCOPE_VIOLATION") + alias(libs.plugins.ksp) id("com.rickclephas.kmp.nativecoroutines") } @@ -24,6 +26,7 @@ kotlin { sourceSets { all { languageSettings.optIn("kotlin.RequiresOptIn") + languageSettings.optIn("kotlin.experimental.ExperimentalObjCName") } val commonMain by getting { dependencies { diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/Clock.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/Clock.kt index ecc6fac9..b2d77f42 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/Clock.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/Clock.kt @@ -1,5 +1,6 @@ package com.rickclephas.kmp.nativecoroutines.sample +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.DelicateCoroutinesApi @@ -13,6 +14,7 @@ object Clock { @Suppress("ObjectPropertyName") private val _time = MutableStateFlow(0L) + @NativeCoroutines val time = _time.asStateFlow() init { @@ -25,4 +27,4 @@ object Clock { } } -expect fun epochSeconds(): Long \ No newline at end of file +expect fun epochSeconds(): Long diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt index 2c1165a0..ca76b39d 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt @@ -1,11 +1,13 @@ package com.rickclephas.kmp.nativecoroutines.sample +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import kotlinx.coroutines.delay import kotlin.random.Random import kotlin.time.Duration.Companion.seconds object RandomLettersGenerator { + @NativeCoroutines suspend fun getRandomLetters(throwException: Boolean): String { delay(2.seconds) if (throwException) { @@ -17,4 +19,4 @@ object RandomLettersGenerator { } return chars.joinToString("") } -} \ No newline at end of file +} diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/issues/GH51.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/issues/GH51.kt index b944da4c..497ad364 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/issues/GH51.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/issues/GH51.kt @@ -1,14 +1,19 @@ package com.rickclephas.kmp.nativecoroutines.sample.issues +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import kotlinx.coroutines.flow.* interface InterfaceA { + @NativeCoroutines fun foo(value: Int): Flow + @NativeCoroutines val bar: Flow } interface InterfaceB { + @NativeCoroutines fun foo(value: Int): Flow + @NativeCoroutines val bar: Flow } @@ -16,7 +21,9 @@ interface InterfaceC: InterfaceA, InterfaceB { // We need to override these else the compiler plugin won't generate `fooNative` and `barNative`. // Those must be overridden since both `InterfaceA` and `InterfaceB` have their own implementations. // Note: all implementations are actually identical, but that isn't known by the compiler. + @NativeCoroutines override fun foo(value: Int): Flow + @NativeCoroutines override val bar: Flow } @@ -29,11 +36,13 @@ abstract class ClassC: InterfaceA, InterfaceB { // We need to override these else the compiler plugin won't generate `fooNative` and `barNative`. // Those must be overridden since both `InterfaceA` and `InterfaceB` have their own implementations. // Note: all implementations are actually identical, but that isn't known by the compiler. + @NativeCoroutines abstract override fun foo(value: Int): Flow + @NativeCoroutines abstract override val bar: Flow } class ClassCImpl: ClassC() { override fun foo(value: Int): Flow = flow { emit(value) } override val bar: Flow = flow { emit(1) } -} \ No newline at end of file +} diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt index fbf6ad7b..fdf9bc38 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt @@ -1,41 +1,50 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class CompilerIntegrationTests: IntegrationTests() { + @NativeCoroutines suspend fun returnGenericClassValue(value: V): V { return value } + @NativeCoroutines suspend fun returnDefaultValue(value: Int = 1): Int { return value } + @NativeCoroutines suspend fun returnGenericValue(value: T): T { return value } fun returnAppendable(value: String): Appendable = StringBuilder(value) + @NativeCoroutines suspend fun returnConstrainedGenericValue(value: T): T { return value } + @NativeCoroutines suspend fun returnGenericValues(values: List): List { return values } + @NativeCoroutines suspend fun returnGenericVarargValues(vararg values: T): Array { return values } + @NativeCoroutines suspend fun List.returnGenericValueFromExtension(value: T): T { return value } + @NativeCoroutines fun returnGenericFlow(value: T): Flow = flow { emit(value) } @@ -44,4 +53,4 @@ class CompilerIntegrationTests: IntegrationTests() { suspend fun returnIgnoredValue(value: Int): Int { return value } -} \ No newline at end of file +} diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt index 86a475c9..16263d8e 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt @@ -1,17 +1,25 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow class FlowIntegrationTests: IntegrationTests() { + private var _emittedCount = 0 + val emittedCount: Int get() = _emittedCount + + @NativeCoroutines fun getFlow(count: Int, delay: Long) = flow { + _emittedCount = 0 repeat(count) { delay(delay) emit(it) + _emittedCount++ } } + @NativeCoroutines fun getFlowWithNull(count: Int, nullIndex: Int, delay: Long) = flow { repeat(count) { delay(delay) @@ -19,6 +27,7 @@ class FlowIntegrationTests: IntegrationTests() { } } + @NativeCoroutines fun getFlowWithException(count: Int, exceptionIndex: Int, message: String, delay: Long) = flow { repeat(count) { delay(delay) @@ -27,6 +36,7 @@ class FlowIntegrationTests: IntegrationTests() { } } + @NativeCoroutines fun getFlowWithError(count: Int, errorIndex: Int, message: String, delay: Long) = flow { repeat(count) { delay(delay) @@ -35,6 +45,7 @@ class FlowIntegrationTests: IntegrationTests() { } } + @NativeCoroutines fun getFlowWithCallback(count: Int, callbackIndex: Int, delay: Long, callback: () -> Unit) = flow { repeat(count) { delay(delay) @@ -42,4 +53,4 @@ class FlowIntegrationTests: IntegrationTests() { emit(it) } } -} \ No newline at end of file +} diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/NewMemoryModelIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/NewMemoryModelIntegrationTests.kt index 10a7aabf..c3f1e744 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/NewMemoryModelIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/NewMemoryModelIntegrationTests.kt @@ -1,5 +1,6 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlin.random.Random @@ -19,6 +20,7 @@ class NewMemoryModelIntegrationTests: IntegrationTests() { return chars.joinToString("") } + @NativeCoroutines suspend fun generateRandomMutableData(): MutableData { val data = MutableData() withContext(Dispatchers.Main) { @@ -29,4 +31,4 @@ class NewMemoryModelIntegrationTests: IntegrationTests() { } return data } -} \ No newline at end of file +} diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt index 6cb25cb0..d05673b9 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt @@ -1,36 +1,43 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class SuspendIntegrationTests: IntegrationTests() { + @NativeCoroutines suspend fun returnValue(value: Int, delay: Long): Int { delay(delay) return value } + @NativeCoroutines suspend fun returnNull(delay: Long): Int? { delay(delay) return null } + @NativeCoroutines suspend fun throwException(message: String, delay: Long): Int { delay(delay) throw Exception(message) } + @NativeCoroutines suspend fun throwError(message: String, delay: Long): Int { delay(delay) throw Error(message) } + @NativeCoroutines suspend fun returnFromCallback(delay: Long, callback: () -> Int): Int { delay(delay) return callback() } + @NativeCoroutines suspend fun getFlow(count: Int, delay: Long): Flow { delay(delay) return flow { diff --git a/settings.gradle.kts b/settings.gradle.kts index bd7149a8..bf72a419 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,3 +12,4 @@ include(":kmp-nativecoroutines-annotations") include(":kmp-nativecoroutines-compiler") include(":kmp-nativecoroutines-compiler-embeddable") include(":kmp-nativecoroutines-gradle-plugin") +include(":kmp-nativecoroutines-ksp")