diff --git a/.github/package.xcworkspace/contents.xcworkspacedata b/.github/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..0fd0bc3 --- /dev/null +++ b/.github/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.github/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.github/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/.github/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved b/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..0368460 --- /dev/null +++ b/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,41 @@ +{ + "pins" : [ + { + "identity" : "swift-macro-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-macro-testing.git", + "state" : { + "revision" : "10dcef36314ddfea6f60442169b0b320204cbd35", + "version" : "0.2.2" + } + }, + { + "identity" : "swift-macro-toolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-macro-toolkit.git", + "state" : { + "revision" : "106daeb38eb3f52b1540aed981fc63fa22274576", + "version" : "0.3.1" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "8e68404f641300bfd0e37d478683bb275926760c", + "version" : "1.15.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" + } + } + ], + "version" : 2 +} diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/Interception.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/Interception.xcscheme new file mode 100644 index 0000000..56e193c --- /dev/null +++ b/.github/package.xcworkspace/xcshareddata/xcschemes/Interception.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/InterceptionTests.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/InterceptionTests.xcscheme new file mode 100644 index 0000000..2c211d8 --- /dev/null +++ b/.github/package.xcworkspace/xcshareddata/xcschemes/InterceptionTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme new file mode 100644 index 0000000..1695afb --- /dev/null +++ b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme new file mode 100644 index 0000000..352ae6c --- /dev/null +++ b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme new file mode 100644 index 0000000..2127e55 --- /dev/null +++ b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c54d8cb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + library-swift-latest: + name: Library + if: | + !contains(github.event.head_commit.message, '[ci skip]') && + !contains(github.event.head_commit.message, '[ci skip test]') && + !contains(github.event.head_commit.message, '[ci skip library-swift-latest]') + runs-on: macos-13 + timeout-minutes: 30 + strategy: + matrix: + config: + - debug + - release + steps: + - uses: actions/checkout@v4 + - name: Select Xcode 15.1 + run: sudo xcode-select -s /Applications/Xcode_15.1.app + - name: Run tests + run: make CONFIG=debug test-library diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..b825518 --- /dev/null +++ b/.spi.yml @@ -0,0 +1 @@ +version: 1 diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Interception.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Interception.xcscheme new file mode 100644 index 0000000..56e193c --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Interception.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/InterceptionTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/InterceptionTests.xcscheme new file mode 100644 index 0000000..2c211d8 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/InterceptionTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme new file mode 100644 index 0000000..1695afb --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme new file mode 100644 index 0000000..352ae6c --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme new file mode 100644 index 0000000..2127e55 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ACKNOWLEDGMENTS b/ACKNOWLEDGMENTS new file mode 100644 index 0000000..7c348e2 --- /dev/null +++ b/ACKNOWLEDGMENTS @@ -0,0 +1,83 @@ +Inspiration and some implementations are taken from: + +–––––––––––––––––––––––––––––– +https://github.com/ReactiveCocoa/ReactiveSwift + +LICENCE +------------------------------ +Copyright (c) 2012 - 2016, GitHub, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +–––––––––––––––––––––––––––––– + +–––––––––––––––––––––––––––––– +https://github.com/ReactiveCocoa/ReactiveCocoa + +LICENCE +------------------------------ +Copyright (c) 2012 - 2016, GitHub, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +–––––––––––––––––––––––––––––– + +–––––––––––––––––––––––––––––– +https://github.com/CombineCommunity/CombineExt + +LICENCE +------------------------------ +Copyright (c) 2020 Combine Community, and/or Shai Mishali + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +–––––––––––––––––––––––––––––– + +–––––––––––––––––––––––––––––– +https://github.com/CombineCommunity/CombineCocoa + +LICENCE +------------------------------ +MIT License + +Copyright (c) 2019 Shai Mishali + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +–––––––––––––––––––––––––––––– diff --git a/LICENCE.md b/LICENCE.md new file mode 100644 index 0000000..929aa26 --- /dev/null +++ b/LICENCE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 CaptureContext + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..317f5de --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +CONFIG = debug +PLATFORM_MACOS = macOS + +default: test + +test: + $(MAKE) CONFIG=debug test-library + +test-library: + (xcodebuild test \ + -skipMacroValidation \ + -configuration $(CONFIG) \ + -workspace .github/package.xcworkspace \ + -scheme InterceptionTests \ + -destination platform=macOS | xcpretty && exit 0 \ + ) \ + || exit 1; diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..163e1f7 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,50 @@ +{ + "pins" : [ + { + "identity" : "swift-interception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/capturecontext/swift-interception.git", + "state" : { + "revision" : "d4d2f902435e2d088a76d24b6fe08d258e209820", + "version" : "0.0.1" + } + }, + { + "identity" : "swift-macro-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-macro-testing.git", + "state" : { + "revision" : "10dcef36314ddfea6f60442169b0b320204cbd35", + "version" : "0.2.2" + } + }, + { + "identity" : "swift-macro-toolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-macro-toolkit.git", + "state" : { + "revision" : "106daeb38eb3f52b1540aed981fc63fa22274576", + "version" : "0.3.1" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "8e68404f641300bfd0e37d478683bb275926760c", + "version" : "1.15.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..8512c5c --- /dev/null +++ b/Package.swift @@ -0,0 +1,63 @@ +// swift-tools-version: 5.9 + +import PackageDescription +import CompilerPluginSupport + +let package = Package( + name: "swift-interception-macros", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .macCatalyst(.v13), + .watchOS(.v6) + ], + products: [ + .library( + name: "InterceptionMacros", + type: .static, + targets: ["InterceptionMacros"] + ) + ], + dependencies: [ + .package( + url: "https://github.com/capturecontext/swift-interception.git", + .upToNextMinor(from: "0.0.1") + ), + .package( + url: "https://github.com/stackotter/swift-macro-toolkit.git", + .upToNextMinor(from: "0.3.0") + ), + .package( + url: "https://github.com/pointfreeco/swift-macro-testing.git", + .upToNextMinor(from: "0.2.2") + ) + ], + targets: [ + .macro( + name: "_InterceptionMacros", + dependencies: [ + .product( + name: "MacroToolkit", + package: "swift-macro-toolkit" + ) + ] + ), + + .target( + name: "InterceptionMacros", + dependencies: [ + .target(name: "_InterceptionMacros"), + .product(name: "Interception", package: "swift-interception") + ] + ), + + .testTarget( + name: "_InterceptionMacrosTests", + dependencies: [ + .target(name: "_InterceptionMacros"), + .product(name: "MacroTesting", package: "swift-macro-testing"), + ] + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..4251945 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# swift-interception + +[![SwiftPM 5.9](https://img.shields.io/badge/swiftpm-5.9-ED523F.svg?style=flat)](https://swift.org/download/) ![Platforms](https://img.shields.io/badge/Platforms-iOS_13_|_macOS_10.15_|_Catalyst_13_|_tvOS_13_|_watchOS_7-ED523F.svg?style=flat) [![@capturecontext](https://img.shields.io/badge/contact-@capturecontext-1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/capture_context) + +Package for interception of objc selectors in Swift. + +## Usage + +### Basic + +Observe any selectors on NSObject instances + +```swift +import Interception + +navigationController.setInterceptionHandler( + for: #methodSelector(UINavigationController.popViewController) +) { result in + print(result.args) // `animated` flag + print(result.output) // popped `UIViewController?`` +} +``` + +You can set up multiple interception handlers as well, just make sure that you use different keys for each handler + +```swift +import Interception + +object.setInterceptionHandler( + for: #methodSelector(MyObject.someMethod(arg1:arg2)), + key: "argumentsPrinter" +) { result in + // In case of multiple arguments + // you can access them as a tuple + print(result.args.0) + print(result.args.1) +} +``` + +### Library development + +If you use it to create a library it may be a good idea to export custom selectors implicitly + +```swift +// Exports.swift +@_exported import _SwiftInterceptionCustomSelectors +``` + +Also you may find some `@_spi` methods and Utils helpful + +```swift +@_spi(Internals) import Interception +import _SwiftInterceptionUtils // Is not shown in the autocomplete +``` + + + +## Installation + +### Basic + +You can add CombineInterception to an Xcode project by adding it as a package dependency. + +1. From the **File** menu, select **Swift Packages › Add Package Dependency…** +2. Enter [`"https://github.com/capturecontext/swift-interception.git"`](https://github.com/capturecontext/swift-interception.git) into the package repository URL text field +3. Choose products you need to link them to your project. + +### Recommended + +If you use SwiftPM for your project, you can add CombineInterception to your package file. + +```swift +.package( + url: "https://github.com/capturecontext/swift-interception.git", + branch: "main" +) +``` + +Do not forget about target dependencies: + +```swift +.product( + name: "Interception", + package: "swift-interception" +) +``` + +## License + +This library is released under the MIT license. See [LICENCE](LICENCE) for details. + +See [ACKNOWLEDGMENTS](ACKNOWLEDGMENTS) for inspiration references and their licences. diff --git a/Sources/InterceptionMacros/Exports.swift b/Sources/InterceptionMacros/Exports.swift new file mode 100644 index 0000000..a731c32 --- /dev/null +++ b/Sources/InterceptionMacros/Exports.swift @@ -0,0 +1 @@ +@_exported import Interception diff --git a/Sources/InterceptionMacros/Macros.swift b/Sources/InterceptionMacros/Macros.swift new file mode 100644 index 0000000..9d12dee --- /dev/null +++ b/Sources/InterceptionMacros/Macros.swift @@ -0,0 +1,73 @@ +import _InterceptionsMacros +import Foundation + +// MARK: - PropertySelectors + +@freestanding(expression) +public macro propertySelector( + _: KeyPath +) -> _ReadonlyPropertySelector = +#externalMacro(module: "_InterceptionsMacros", type: "PropertySelectorMacro") + +@freestanding(expression) +public macro propertySelector( + _: WritableKeyPath +) -> _MutablePropertySelector = +#externalMacro(module: "_InterceptionsMacros", type: "PropertySelectorMacro") + +// MARK: - MethodSelectors + +@freestanding(expression) +public macro methodSelector( + _: (() -> Output)? +) -> _MethodSelector = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: ((Arg) -> Output)? +) -> _MethodSelector = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: ((repeat each Arg) -> Output)? +) -> _MethodSelector<(repeat each Arg), Output> = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: (Object) -> (() -> Output)? +) -> _MethodSelector = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: (Object) -> ((Arg) -> Output)? +) -> _MethodSelector = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: (Object) -> ((repeat each Arg) -> Output)? +) -> _MethodSelector<(repeat each Arg), Output> = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: (Object) -> () -> Output +) -> _MethodSelector = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: (Object) -> (Arg) -> Output +) -> _MethodSelector = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + +@freestanding(expression) +public macro methodSelector( + _: (Object) -> (repeat each Arg) -> Output +) -> _MethodSelector<(repeat each Arg), Output> = +#externalMacro(module: "_InterceptionsMacros", type: "MethodSelectorMacro") + diff --git a/Sources/_InterceptionMacros/Macros/MethodSelectorMacro.swift b/Sources/_InterceptionMacros/Macros/MethodSelectorMacro.swift new file mode 100644 index 0000000..0fc687b --- /dev/null +++ b/Sources/_InterceptionMacros/Macros/MethodSelectorMacro.swift @@ -0,0 +1,24 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros +import Foundation + +public struct MethodSelectorMacro: ExpressionMacro { + public static func expansion< + Node: FreestandingMacroExpansionSyntax, + Context: MacroExpansionContext + >( + of node: Node, + in context: Context + ) -> ExprSyntax { + guard let arg = node.argumentList.first.map(\.expression) + else { fatalError("compiler bug: the macro does not have any arguments") } + + return """ + _makeMethodSelector( + selector: #selector(\(arg)), + signature: \(arg) + ) + """ + } +} diff --git a/Sources/_InterceptionMacros/Macros/PropertySelectorMacro.swift b/Sources/_InterceptionMacros/Macros/PropertySelectorMacro.swift new file mode 100644 index 0000000..e9ce530 --- /dev/null +++ b/Sources/_InterceptionMacros/Macros/PropertySelectorMacro.swift @@ -0,0 +1,20 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +public struct PropertySelectorMacro: ExpressionMacro { + public static func expansion< + Node: FreestandingMacroExpansionSyntax, + Context: MacroExpansionContext + >( + of node: Node, + in context: Context + ) -> ExprSyntax { + guard let arg = node.argumentList.first.map(\.expression) + else { fatalError("compiler bug: the macro does not have any arguments") } + + return """ + _unsafeMakePropertySelector(\(arg)) + """ + } +} diff --git a/Sources/_InterceptionMacros/Plugin.swift b/Sources/_InterceptionMacros/Plugin.swift new file mode 100644 index 0000000..5599cdf --- /dev/null +++ b/Sources/_InterceptionMacros/Plugin.swift @@ -0,0 +1,11 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct Plugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + MethodSelectorMacro.self, + PropertySelectorMacro.self + ] +} + diff --git a/Tests/_InterceptionMacrosTests/MethodSelectorTests.swift b/Tests/_InterceptionMacrosTests/MethodSelectorTests.swift new file mode 100644 index 0000000..6eee673 --- /dev/null +++ b/Tests/_InterceptionMacrosTests/MethodSelectorTests.swift @@ -0,0 +1,31 @@ +import XCTest +import MacroTesting +import _SwiftInterceptionCustomSelectorsMacros + +final class MethodSelectorTests: XCTestCase { + override func invokeTest() { + withMacroTesting( + isRecording: false, + macros: [ + "methodSelector": MethodSelectorMacro.self + ] + ) { + super.invokeTest() + } + } + + func testApplication() { + assertMacro { + """ + #methodSelector(Object.someFunc) + """ + } expansion: { + """ + _makeMethodSelector( + selector: #selector(Object.someFunc), + signature: Object.someFunc + ) + """ + } + } +} diff --git a/Tests/_InterceptionMacrosTests/PropertySelectorTests.swift b/Tests/_InterceptionMacrosTests/PropertySelectorTests.swift new file mode 100644 index 0000000..8963af4 --- /dev/null +++ b/Tests/_InterceptionMacrosTests/PropertySelectorTests.swift @@ -0,0 +1,28 @@ +import XCTest +import MacroTesting +import _SwiftInterceptionCustomSelectorsMacros + +final class PropertySelectorTests: XCTestCase { + override func invokeTest() { + withMacroTesting( + isRecording: false, + macros: [ + "propertySelector": PropertySelectorMacro.self + ] + ) { + super.invokeTest() + } + } + + func testApplication() { + assertMacro { + #""" + #propertySelector(\Object.someProperty) + """# + } expansion: { + #""" + _unsafeMakePropertySelector(\Object.someProperty) + """# + } + } +}