├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── AssociatedObject.podspec ├── Binary └── .gitkeep ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── AssociatedObject │ ├── AssociatedObject.swift │ ├── Extension │ │ ├── objc_AssociationPolicy+.swift │ │ └── swift_AssociationPolicy+.swift │ └── functions.swift ├── AssociatedObjectC │ ├── associated_object_key.c │ └── include │ │ └── associated_object_key.h └── AssociatedObjectPlugin │ ├── AssociatedObjectMacro.swift │ ├── AssociatedObjectMacroDiagnostic.swift │ ├── AssociatedObjectMacrosPlugin.swift │ └── Extension │ ├── PatternBindingSyntax+.swift │ └── TypeSyntax+.swift ├── Tests └── AssociatedObjectTests │ ├── AssociatedObjectTests.swift │ ├── AssociatedTypeDetectionObjectTests.swift │ ├── Model │ └── ClassType.swift │ └── PatternBindingSyntax+Tests.swift └── image.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - README.md 9 | - LICENSE 10 | pull_request: 11 | paths-ignore: 12 | - README.md 13 | - LICENSE 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | 19 | env: 20 | DEVELOPER_DIR: /Applications/Xcode_16.3.app 21 | 22 | jobs: 23 | build: 24 | name: Build & Test 25 | runs-on: macos-15 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Select Xcode 16 31 | run: sudo xcode-select -s /Applications/Xcode_16.3.app 32 | 33 | - name: Build 34 | run: swift build 35 | 36 | - name: Test 37 | run: swift test 38 | 39 | linux-test: 40 | name: Linux Test 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Install Swift 44 | uses: swift-actions/setup-swift@v2 45 | with: 46 | swift-version: '6.1.0' 47 | 48 | - uses: actions/checkout@v4 49 | 50 | - name: Test 51 | run: swift test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | Binary/AssociatedObjectPlugin 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AssociatedObject.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "AssociatedObject" 4 | s.version = "0.10.3" 5 | s.summary = "Swift Macro for allowing variable declarations even in class extensions." 6 | 7 | s.description = <<-DESC 8 | Swift Macro for allowing variable declarations even in class extensions. 9 | DESC 10 | 11 | s.homepage = "https://github.com/p-x9/AssociatedObject" 12 | s.license = { :type => "MIT", :file => "LICENSE" } 13 | s.author = { "p-x9" => "https://github.com/p-x9" } 14 | 15 | s.ios.deployment_target = "13.0" 16 | s.tvos.deployment_target = "13.0" 17 | s.osx.deployment_target = "10.15" 18 | s.watchos.deployment_target = "6.0" 19 | 20 | s.source = { :git => "https://github.com/p-x9/AssociatedObject.git", :tag => "#{s.version}" } 21 | 22 | s.prepare_command = 'swift build -c release && cp -f .build/release/AssociatedObjectPlugin ./Binary' 23 | 24 | s.source_files = "Sources/AssociatedObject/**/*.{c,h,m,swift}", 'Sources/AssociatedObjectC/**/*.{c,h,m,swift}' 25 | s.swift_versions = "5.9" 26 | 27 | # CocoaPods do not support Linux 28 | # s.dependency "ObjectAssociation", "0.5.0" 29 | 30 | s.preserve_paths = ["Binary/AssociatedObjectPlugin"] 31 | s.pod_target_xcconfig = { 32 | 'OTHER_SWIFT_FLAGS' => [ 33 | '-load-plugin-executable ${PODS_ROOT}/AssociatedObject/Binary/AssociatedObjectPlugin#AssociatedObjectPlugin' 34 | ] 35 | } 36 | s.user_target_xcconfig = { 37 | 'OTHER_SWIFT_FLAGS' => [ 38 | '-load-plugin-executable ${PODS_ROOT}/AssociatedObject/Binary/AssociatedObjectPlugin#AssociatedObjectPlugin' 39 | ] 40 | } 41 | 42 | end 43 | -------------------------------------------------------------------------------- /Binary/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/AssociatedObject/ba5859d881ac1b814e77e2f1b39235dfe71a8d1f/Binary/.gitkeep -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 p-x9 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-custom-dump", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 7 | "state" : { 8 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 9 | "version" : "1.3.3" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-literal-type-inference", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/p-x9/swift-literal-type-inference.git", 16 | "state" : { 17 | "revision" : "7235a8e47b257bbd54da76191b043976509c4b8c", 18 | "version" : "0.4.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-macro-testing", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 25 | "state" : { 26 | "revision" : "cfe474c7e97d429ea31eefed2e9ab8c7c74260f9", 27 | "version" : "0.6.2" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-object-association", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/p-x9/swift-object-association.git", 34 | "state" : { 35 | "revision" : "93806cfecae1f198c894ed5585b93aff0e2d1f5a", 36 | "version" : "0.5.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-snapshot-testing", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 43 | "state" : { 44 | "revision" : "1be8144023c367c5de701a6313ed29a3a10bf59b", 45 | "version" : "1.18.3" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-syntax", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/swiftlang/swift-syntax.git", 52 | "state" : { 53 | "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", 54 | "version" : "601.0.1" 55 | } 56 | }, 57 | { 58 | "identity" : "xctest-dynamic-overlay", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 61 | "state" : { 62 | "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", 63 | "version" : "1.5.2" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | 6 | let package = Package( 7 | name: "AssociatedObject", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15) 11 | ], 12 | products: [ 13 | .library( 14 | name: "AssociatedObject", 15 | targets: ["AssociatedObject"] 16 | ), 17 | ], 18 | dependencies: [ 19 | .package( 20 | url: "https://github.com/swiftlang/swift-syntax.git", 21 | "509.0.0"..<"602.0.0" 22 | ), 23 | .package( 24 | url: "https://github.com/p-x9/swift-literal-type-inference.git", 25 | from: "0.4.0" 26 | ), 27 | .package( 28 | url: "https://github.com/p-x9/swift-object-association.git", 29 | from: "0.5.0" 30 | ), 31 | .package( 32 | url: "https://github.com/pointfreeco/swift-macro-testing.git", 33 | from: "0.6.1" 34 | ) 35 | 36 | ], 37 | targets: [ 38 | .target( 39 | name: "AssociatedObject", 40 | dependencies: [ 41 | "AssociatedObjectC", 42 | "AssociatedObjectPlugin", 43 | .product( 44 | name: "ObjectAssociation", 45 | package: "swift-object-association", 46 | condition: .when( 47 | platforms: [ 48 | .linux, .openbsd, .windows, .android 49 | ] 50 | ) 51 | ) 52 | ] 53 | ), 54 | .target( 55 | name: "AssociatedObjectC" 56 | ), 57 | .macro( 58 | name: "AssociatedObjectPlugin", 59 | dependencies: [ 60 | .product(name: "SwiftSyntax", package: "swift-syntax"), 61 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 62 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 63 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 64 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"), 65 | .product( 66 | name: "LiteralTypeInference", 67 | package: "swift-literal-type-inference" 68 | ) 69 | ] 70 | ), 71 | .testTarget( 72 | name: "AssociatedObjectTests", 73 | dependencies: [ 74 | "AssociatedObject", 75 | "AssociatedObjectPlugin", 76 | .product(name: "SwiftSyntax", package: "swift-syntax"), 77 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 78 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 79 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 80 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 81 | .product(name: "MacroTesting", package: "swift-macro-testing") 82 | ] 83 | ), 84 | ] 85 | ) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AssociatedObject 2 | Swift Macro for allowing variable declarations even in class extensions. 3 | It is implemented by wrapping `objc_getAssociatedObject`/`objc_setAssociatedObject`. 4 | 5 | 6 | 7 | [![Github issues](https://img.shields.io/github/issues/p-x9/AssociatedObject)](https://github.com/p-x9/AssociatedObject/issues) 8 | [![Github forks](https://img.shields.io/github/forks/p-x9/AssociatedObject)](https://github.com/p-x9/AssociatedObject/network/members) 9 | [![Github stars](https://img.shields.io/github/stars/p-x9/AssociatedObject)](https://github.com/p-x9/AssociatedObject/stargazers) 10 | [![Github top language](https://img.shields.io/github/languages/top/p-x9/AssociatedObject)](https://github.com/p-x9/AssociatedObject/) 11 | 12 | ## Installation 13 | 14 | #### SPM 15 | ```swift 16 | .package(url: "https://github.com/p-x9/AssociatedObject", from: "0.10.3") 17 | ``` 18 | 19 | #### CocoaPods 20 | Add below to your `Podfile`. 21 | ``` 22 | pod 'AssociatedObject', git: 'https://github.com/p-x9/AssociatedObject', tag: '0.10.3' 23 | ``` 24 | 25 | After `pod install`, you can use this Macro in your project. 26 | 27 | Additionally, if you encounter build error like `Expansion of macro 'AssociatedObject' did not produce a non-observing accessor`. You should check your project setting `Build Settings`-`OTHER_SWIFT_FLAGS`. 28 | 29 | There should be additional flags like so. 30 | ![Alt text](image.png) 31 | 32 | If not, you can add these two lines by yourself. 33 | ``` 34 | -load-plugin-executable 35 | ${PODS_ROOT}/AssociatedObject/Binary/AssociatedObjectPlugin#AssociatedObjectPlugin 36 | ``` 37 | 38 | ## Usage 39 | For example, you can add a new stored property to `UIViewController` by declaring the following 40 | ```swift 41 | import AssociatedObject 42 | 43 | extension UIViewController { 44 | @AssociatedObject(.retain(nonatomic)) 45 | var text = "text" 46 | 47 | /* OR */ 48 | 49 | @AssociatedObject(.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 50 | var text = "text" 51 | 52 | static var customKey = "" 53 | @AssociatedObject(.OBJC_ASSOCIATION_RETAIN_NONATOMIC, key: customKey) 54 | var somevar = "text" 55 | } 56 | ``` 57 | 58 | Declared properties can be used as follows 59 | ```swift 60 | class ViewController: UIViewController { 61 | override func viewDidLoad() { 62 | super.viewDidLoad() 63 | 64 | print(text) // => "text" 65 | 66 | text = "hello" 67 | print(text) // => "hello" 68 | } 69 | 70 | } 71 | ``` 72 | ### willSet/didSet 73 | Properties defined using `@AssociatedObject` can implement willSet and didSet. 74 | In swift, it is not possible to implement `willSet` and `didSet` at the same time as setter, so they are expanded as follows. 75 | 76 | ```swift 77 | @AssociatedObject(.copy(nonatomic)) 78 | public var hello: String = "こんにちは" { 79 | didSet { 80 | print("didSet") 81 | } 82 | willSet { 83 | print("willSet: \(newValue)") 84 | } 85 | } 86 | 87 | // ↓↓↓ expand to ... ↓↓↓ 88 | public var hello: String = "こんにちは" { 89 | get { 90 | objc_getAssociatedObject( 91 | self, 92 | &Self.__associated_helloKey 93 | ) as? String 94 | ?? "こんにちは" 95 | } 96 | 97 | set { 98 | let willSet: (String) -> Void = { [self] newValue in 99 | print("willSet: \(newValue)") 100 | } 101 | willSet(newValue) 102 | 103 | let oldValue = hello 104 | 105 | objc_setAssociatedObject( 106 | self, 107 | &Self.__associated_helloKey, 108 | newValue, 109 | .copy(nonatomic) 110 | ) 111 | 112 | let didSet: (String) -> Void = { [self] oldValue in 113 | print("didSet") 114 | } 115 | didSet(oldValue) 116 | } 117 | } 118 | ``` 119 | 120 | ## License 121 | AssociatedObject is released under the MIT License. See [LICENSE](./LICENSE) 122 | -------------------------------------------------------------------------------- /Sources/AssociatedObject/AssociatedObject.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AssociatedObjectC) 2 | @_exported import AssociatedObjectC 3 | #endif 4 | 5 | 6 | #if canImport(ObjectiveC) 7 | 8 | @_exported import ObjectiveC 9 | public typealias Policy = objc_AssociationPolicy 10 | 11 | #elseif canImport(ObjectAssociation) 12 | 13 | @_exported import ObjectAssociation 14 | public typealias Policy = swift_AssociationPolicy 15 | 16 | #endif 17 | 18 | 19 | @attached(peer, names: arbitrary) 20 | @attached(accessor) 21 | public macro AssociatedObject( 22 | _ policy: Policy 23 | ) = #externalMacro( 24 | module: "AssociatedObjectPlugin", 25 | type: "AssociatedObjectMacro" 26 | ) 27 | 28 | @attached(peer, names: arbitrary) 29 | @attached(accessor) 30 | public macro AssociatedObject( 31 | _ policy: Policy, 32 | key: Any 33 | ) = #externalMacro( 34 | module: "AssociatedObjectPlugin", 35 | type: "AssociatedObjectMacro" 36 | ) 37 | 38 | @attached(accessor) 39 | public macro _AssociatedObject( 40 | _ policy: Policy 41 | ) = #externalMacro( 42 | module: "AssociatedObjectPlugin", 43 | type: "AssociatedObjectMacro" 44 | ) 45 | -------------------------------------------------------------------------------- /Sources/AssociatedObject/Extension/objc_AssociationPolicy+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // objc_AssociationPolicy+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/04. 6 | // 7 | // 8 | 9 | #if canImport(ObjectiveC) 10 | 11 | import ObjectiveC 12 | 13 | /// Extension for objc_AssociationPolicy to provide a more Swift-friendly interface. 14 | extension objc_AssociationPolicy { 15 | /// Represents the atomicity options for associated objects. 16 | public enum Atomicity { 17 | /// Indicates that the associated object should be stored atomically. 18 | case atomic 19 | /// Indicates that the associated object should be stored non-atomically. 20 | case nonatomic 21 | } 22 | 23 | /// A property wrapper that corresponds to `.OBJC_ASSOCIATION_ASSIGN` policy. 24 | public static var assign: Self { .OBJC_ASSOCIATION_ASSIGN } 25 | 26 | /// Create an association policy for retaining an associated object with the specified atomicity. 27 | /// 28 | /// - Parameter atomicity: The desired atomicity for the associated object. 29 | /// - Returns: The appropriate association policy for retaining with the specified atomicity. 30 | public static func retain(_ atomicity: Atomicity) -> Self { 31 | switch atomicity { 32 | case .atomic: 33 | return .OBJC_ASSOCIATION_RETAIN 34 | case .nonatomic: 35 | return .OBJC_ASSOCIATION_RETAIN_NONATOMIC 36 | } 37 | } 38 | 39 | /// Create an association policy for copying an associated object with the specified atomicity. 40 | /// 41 | /// - Parameter atomicity: The desired atomicity for the associated object. 42 | /// - Returns: The appropriate association policy for copying with the specified atomicity. 43 | public static func copy(_ atomicity: Atomicity) -> Self { 44 | switch atomicity { 45 | case .atomic: 46 | return .OBJC_ASSOCIATION_COPY 47 | case .nonatomic: 48 | return .OBJC_ASSOCIATION_COPY_NONATOMIC 49 | } 50 | } 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /Sources/AssociatedObject/Extension/swift_AssociationPolicy+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // swift_AssociationPolicy+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/01/29. 6 | // 7 | // 8 | 9 | #if !canImport(ObjectiveC) && canImport(ObjectAssociation) 10 | 11 | import ObjectAssociation 12 | 13 | /// Extension for swift_AssociationPolicy to provide a more Swift-friendly interface. 14 | extension swift_AssociationPolicy { 15 | /// Represents the atomicity options for associated objects. 16 | public enum Atomicity { 17 | /// Indicates that the associated object should be stored atomically. 18 | case atomic 19 | /// Indicates that the associated object should be stored non-atomically. 20 | case nonatomic 21 | } 22 | 23 | /// A property wrapper that corresponds to `.SWIFT_ASSOCIATION_ASSIGN` policy. 24 | public static var assign: Self { .SWIFT_ASSOCIATION_ASSIGN } 25 | 26 | /// A property wrapper that corresponds to `.SWIFT_ASSOCIATION_WEAK` policy. 27 | public static var weak: Self { .SWIFT_ASSOCIATION_WEAK } 28 | 29 | /// Create an association policy for retaining an associated object with the specified atomicity. 30 | /// 31 | /// - Parameter atomicity: The desired atomicity for the associated object. 32 | /// - Returns: The appropriate association policy for retaining with the specified atomicity. 33 | public static func retain(_ atomicity: Atomicity) -> Self { 34 | switch atomicity { 35 | case .atomic: 36 | return .SWIFT_ASSOCIATION_RETAIN 37 | case .nonatomic: 38 | return .SWIFT_ASSOCIATION_RETAIN_NONATOMIC 39 | } 40 | } 41 | } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/AssociatedObject/functions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // functions.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/01/28. 6 | // 7 | // 8 | 9 | #if canImport(ObjectiveC) 10 | import ObjectiveC 11 | #elseif canImport(ObjectAssociation) 12 | import ObjectAssociation 13 | #else 14 | #warning("Current platform is not supported") 15 | #endif 16 | 17 | 18 | public func getAssociatedObject( 19 | _ object: AnyObject, 20 | _ key: UnsafeRawPointer 21 | ) -> Any? { 22 | #if canImport(ObjectiveC) 23 | objc_getAssociatedObject(object, key) 24 | #elseif canImport(ObjectAssociation) 25 | ObjectAssociation.getAssociatedObject(object, key) 26 | #endif 27 | } 28 | 29 | 30 | public func setAssociatedObject( 31 | _ object: AnyObject, 32 | _ key: UnsafeRawPointer, 33 | _ value: Any?, 34 | _ policy: Policy = .retain(.nonatomic) 35 | ) { 36 | #if canImport(ObjectiveC) 37 | objc_setAssociatedObject( 38 | object, 39 | key, 40 | value, 41 | policy 42 | ) 43 | #elseif canImport(ObjectAssociation) 44 | ObjectAssociation.setAssociatedObject( 45 | object, 46 | key, 47 | value 48 | ) 49 | #endif 50 | } 51 | -------------------------------------------------------------------------------- /Sources/AssociatedObjectC/associated_object_key.c: -------------------------------------------------------------------------------- 1 | #include "include/associated_object_key.h" 2 | 3 | NOINLINE 4 | const void *_associated_object_key() { 5 | return get_return_address(); 6 | } 7 | -------------------------------------------------------------------------------- /Sources/AssociatedObjectC/include/associated_object_key.h: -------------------------------------------------------------------------------- 1 | // 2 | // associated_object_key.h 3 | // 4 | // 5 | // Created by p-x9 on 2024/01/26. 6 | // 7 | // 8 | 9 | #ifndef associated_object_key_h 10 | #define associated_object_key_h 11 | 12 | #if defined(__wasm__) 13 | // Wasm can't access call frame for security purposes 14 | #define get_return_address() ((void*) 0) 15 | #elif __GNUC__ 16 | #define get_return_address() __builtin_return_address(0) 17 | #elif _MSC_VER 18 | #include 19 | #define get_return_address() _ReturnAddress() 20 | #else 21 | #error missing implementation for get_return_address 22 | #define get_return_address() ((void*) 0) 23 | #endif 24 | 25 | #ifdef __GNUC__ 26 | #define NOINLINE __attribute__((noinline)) 27 | #elif _MSC_VER 28 | #define NOINLINE __declspec(noinline) 29 | #else 30 | #error missing implementation for `NOINLINE` 31 | #define NOINLINE 32 | #endif 33 | 34 | const void *_associated_object_key(); 35 | 36 | #endif /* associated_object_key_h */ 37 | -------------------------------------------------------------------------------- /Sources/AssociatedObjectPlugin/AssociatedObjectMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedObjectMacro.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/12. 6 | // 7 | // 8 | 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | import SwiftSyntaxMacros 12 | import LiteralTypeInference 13 | 14 | public struct AssociatedObjectMacro {} 15 | 16 | extension AssociatedObjectMacro: PeerMacro { 17 | public static func expansion( 18 | of node: AttributeSyntax, 19 | providingPeersOf declaration: Declaration, 20 | in context: Context 21 | ) throws -> [DeclSyntax] { 22 | 23 | guard let varDecl = declaration.as(VariableDeclSyntax.self), 24 | let binding = varDecl.bindings.first, 25 | let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { 26 | context.diagnose(AssociatedObjectMacroDiagnostic.requiresVariableDeclaration.diagnose(at: declaration)) 27 | return [] 28 | } 29 | 30 | if case let .argumentList(arguments) = node.arguments, 31 | let element = arguments.first(where: { $0.label?.text == "key" }) { 32 | let kind = element.expression.kind 33 | if ![.declReferenceExpr, .memberAccessExpr].contains(kind) { 34 | context.diagnose( 35 | AssociatedObjectMacroDiagnostic 36 | .invalidCustomKeySpecification 37 | .diagnose(at: element) 38 | ) 39 | } 40 | // Provide store key from outside the macro 41 | return [] 42 | } 43 | 44 | let defaultValue = binding.initializer?.value 45 | let type: TypeSyntax? = binding.typeAnnotation?.type ?? defaultValue?.inferredType 46 | 47 | guard let type else { 48 | // Explicit specification of type is required 49 | context.diagnose(AssociatedObjectMacroDiagnostic.specifyTypeExplicitly.diagnose(at: identifier)) 50 | return [] 51 | } 52 | 53 | let keyAccessor = """ 54 | let f: @convention(c) () -> Void = {} 55 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 56 | """ 57 | 58 | let keyDecl = VariableDeclSyntax( 59 | attributes: [ 60 | .attribute("@inline(never)") 61 | ], 62 | bindingSpecifier: .identifier("static var"), 63 | bindings: PatternBindingListSyntax { 64 | PatternBindingSyntax( 65 | pattern: IdentifierPatternSyntax(identifier: .identifier("__associated_\(identifier.trimmed)Key")), 66 | typeAnnotation: .init(type: IdentifierTypeSyntax(name: .identifier("UnsafeRawPointer"))), 67 | accessorBlock: .init( 68 | accessors: .getter("\(raw: keyAccessor)") 69 | ) 70 | ) 71 | } 72 | ) 73 | 74 | var decls = [ 75 | DeclSyntax(keyDecl) 76 | ] 77 | 78 | if type.isOptional && defaultValue != nil { 79 | let flagName = "__associated_\(identifier.trimmed)IsSet" 80 | let flagDecl = VariableDeclSyntax( 81 | attributes: [ 82 | .attribute("@_AssociatedObject(.retain(.nonatomic))") 83 | ], 84 | bindingSpecifier: .identifier("var"), 85 | bindings: PatternBindingListSyntax { 86 | PatternBindingSyntax( 87 | pattern: IdentifierPatternSyntax( 88 | identifier: .identifier(flagName) 89 | ), 90 | typeAnnotation: .init(type: IdentifierTypeSyntax(name: .identifier("Bool"))), 91 | initializer: InitializerClauseSyntax(value: BooleanLiteralExprSyntax(false)) 92 | ) 93 | } 94 | ) 95 | 96 | // nested peer macro will not expand 97 | // https://github.com/apple/swift/issues/69073 98 | let keyAccessor = """ 99 | let f: @convention(c) () -> Void = {} 100 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 101 | """ 102 | let flagKeyDecl = VariableDeclSyntax( 103 | attributes: [ 104 | .attribute("@inline(never)") 105 | ], 106 | bindingSpecifier: .identifier("static var"), 107 | bindings: PatternBindingListSyntax { 108 | PatternBindingSyntax( 109 | pattern: IdentifierPatternSyntax( 110 | identifier: .identifier("__associated___associated_\(identifier.trimmed)IsSetKey") 111 | ), 112 | typeAnnotation: .init(type: IdentifierTypeSyntax(name: .identifier("UnsafeRawPointer"))), 113 | accessorBlock: .init( 114 | accessors: .getter("\(raw: keyAccessor)") 115 | ) 116 | ) 117 | } 118 | ) 119 | decls.append(DeclSyntax(flagDecl)) 120 | decls.append(DeclSyntax(flagKeyDecl)) 121 | } 122 | 123 | return decls 124 | } 125 | } 126 | 127 | extension AssociatedObjectMacro: AccessorMacro { 128 | public static func expansion( 129 | of node: AttributeSyntax, 130 | providingAccessorsOf declaration: Declaration, 131 | in context: Context 132 | ) throws -> [AccessorDeclSyntax] { 133 | 134 | guard let varDecl = declaration.as(VariableDeclSyntax.self), 135 | let binding = varDecl.bindings.first, 136 | let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier 137 | else { 138 | // Probably can't add a diagnose here, since this is an Accessor macro 139 | context.diagnose(AssociatedObjectMacroDiagnostic.requiresVariableDeclaration.diagnose(at: declaration)) 140 | return [] 141 | } 142 | 143 | if varDecl.bindings.count > 1 { 144 | context.diagnose(AssociatedObjectMacroDiagnostic.multipleVariableDeclarationIsNotSupported.diagnose(at: binding)) 145 | return [] 146 | } 147 | 148 | let defaultValue = binding.initializer?.value 149 | let type: TypeSyntax 150 | 151 | if let specifiedType = binding.typeAnnotation?.type { 152 | // TypeAnnotation 153 | type = specifiedType 154 | } else if let inferredType = defaultValue?.inferredType { 155 | // infer type of defaultValue 156 | type = inferredType 157 | } else { 158 | // Explicit specification of type is required 159 | context.diagnose(AssociatedObjectMacroDiagnostic.specifyTypeExplicitly.diagnose(at: identifier)) 160 | return [] 161 | } 162 | 163 | // Error if setter already exists 164 | if let setter = binding.setter { 165 | context.diagnose(AssociatedObjectMacroDiagnostic.getterAndSetterShouldBeNil.diagnose(at: setter)) 166 | return [] 167 | } 168 | 169 | // Error if getter already exists 170 | if let getter = binding.getter { 171 | context.diagnose(AssociatedObjectMacroDiagnostic.getterAndSetterShouldBeNil.diagnose(at: getter)) 172 | return [] 173 | } 174 | 175 | // Initial value required if type is optional 176 | if defaultValue == nil && !type.isOptional { 177 | context.diagnose(AssociatedObjectMacroDiagnostic.requiresInitialValue.diagnose(at: declaration)) 178 | return [] 179 | } 180 | 181 | guard case let .argumentList(arguments) = node.arguments else { 182 | return [] 183 | } 184 | 185 | var policy: ExprSyntax = ".retain(.nonatomic)" 186 | if let firstElement = arguments.first?.expression, 187 | let specifiedPolicy = ExprSyntax(firstElement) { 188 | policy = specifiedPolicy 189 | } 190 | 191 | var associatedKey: ExprSyntax = "Self.__associated_\(identifier.trimmed)Key" 192 | if let element = arguments.first(where: { $0.label?.text == "key" }), 193 | let customKey = ExprSyntax(element.expression) { 194 | // Provide store key from outside the macro 195 | associatedKey = "&\(customKey)" 196 | } 197 | 198 | return [ 199 | Self.getter( 200 | identifier: identifier, 201 | type: type, 202 | associatedKey: associatedKey, 203 | policy: policy, 204 | defaultValue: defaultValue 205 | ), 206 | 207 | Self.setter( 208 | identifier: identifier, 209 | type: type, 210 | policy: policy, 211 | associatedKey: associatedKey, 212 | hasDefaultValue: defaultValue != nil, 213 | willSet: binding.willSet, 214 | didSet: binding.didSet 215 | ) 216 | ] 217 | } 218 | } 219 | 220 | extension AssociatedObjectMacro { 221 | /// Create the syntax for the `get` accessor after expansion. 222 | /// - Parameters: 223 | /// - identifier: Type of Associated object. 224 | /// - type: Type of Associated object. 225 | /// - defaultValue: Syntax of default value 226 | /// - Returns: Syntax of `get` accessor after expansion. 227 | static func getter( 228 | identifier: TokenSyntax, 229 | type: TypeSyntax, 230 | associatedKey: ExprSyntax, 231 | policy: ExprSyntax, 232 | defaultValue: ExprSyntax? 233 | ) -> AccessorDeclSyntax { 234 | let typeWithoutOptional = if let type = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) { 235 | type.wrappedType 236 | } else if let type = type.as(OptionalTypeSyntax.self) { 237 | type.wrappedType 238 | } else { 239 | type 240 | } 241 | 242 | return AccessorDeclSyntax( 243 | accessorSpecifier: .keyword(.get), 244 | body: CodeBlockSyntax { 245 | if let defaultValue, type.isOptional { 246 | """ 247 | if !self.__associated_\(identifier.trimmed)IsSet { 248 | let value: \(type.trimmed) = \(defaultValue.trimmed) 249 | setAssociatedObject( 250 | self, 251 | \(associatedKey), 252 | value, 253 | \(policy.trimmed) 254 | ) 255 | self.__associated_\(identifier.trimmed)IsSet = true 256 | return value 257 | } else { 258 | return getAssociatedObject( 259 | self, 260 | \(associatedKey) 261 | ) as? \(typeWithoutOptional.trimmed) 262 | } 263 | """ 264 | } else if let defaultValue { 265 | """ 266 | if let value = getAssociatedObject( 267 | self, 268 | \(associatedKey) 269 | ) as? \(typeWithoutOptional.trimmed) { 270 | return value 271 | } else { 272 | let value: \(type.trimmed) = \(defaultValue.trimmed) 273 | setAssociatedObject( 274 | self, 275 | \(associatedKey), 276 | value, 277 | \(policy.trimmed) 278 | ) 279 | return value 280 | } 281 | """ 282 | } else { 283 | """ 284 | getAssociatedObject( 285 | self, 286 | \(associatedKey) 287 | ) as? \(typeWithoutOptional.trimmed) 288 | ?? \(defaultValue ?? "nil") 289 | """ 290 | } 291 | } 292 | ) 293 | } 294 | } 295 | 296 | extension AssociatedObjectMacro { 297 | /// Create the syntax for the `set` accessor after expansion. 298 | /// - Parameters: 299 | /// - identifier: Name of associated object. 300 | /// - type: Type of Associated object. 301 | /// - policy: Syntax of `objc_AssociationPolicy` 302 | /// - `willSet`: `willSet` accessor of the original variable definition. 303 | /// - `didSet`: `didSet` accessor of the original variable definition. 304 | /// - Returns: Syntax of `set` accessor after expansion. 305 | static func setter( 306 | identifier: TokenSyntax, 307 | type: TypeSyntax, 308 | policy: ExprSyntax, 309 | associatedKey: ExprSyntax, 310 | hasDefaultValue: Bool, 311 | `willSet`: AccessorDeclSyntax?, 312 | `didSet`: AccessorDeclSyntax? 313 | ) -> AccessorDeclSyntax { 314 | AccessorDeclSyntax( 315 | accessorSpecifier: .keyword(.set), 316 | body: CodeBlockSyntax { 317 | if let willSet = `willSet`, 318 | let body = willSet.body { 319 | Self.willSet( 320 | type: type, 321 | accessor: willSet, 322 | body: body 323 | ) 324 | 325 | Self.callWillSet() 326 | .with(\.trailingTrivia, .newlines(2)) 327 | } 328 | 329 | if `didSet` != nil { 330 | "let oldValue = \(identifier)" 331 | } 332 | 333 | """ 334 | setAssociatedObject( 335 | self, 336 | \(associatedKey), 337 | newValue, 338 | \(policy) 339 | ) 340 | """ 341 | if type.isOptional, hasDefaultValue { 342 | """ 343 | self.__associated_\(identifier.trimmed)IsSet = true 344 | """ 345 | } 346 | 347 | if let didSet = `didSet`, 348 | let body = didSet.body { 349 | Self.didSet( 350 | type: type, 351 | accessor: didSet, 352 | body: body 353 | ).with(\.leadingTrivia, .newlines(2)) 354 | 355 | Self.callDidSet() 356 | } 357 | } 358 | ) 359 | } 360 | 361 | /// `willSet` closure 362 | /// 363 | /// Convert a willSet accessor to a closure variable in the following format. 364 | /// ```swift 365 | /// let `willSet`: (\(type.trimmed)) -> Void = { [self] \(newValue) in 366 | /// \(body.statements.trimmed) 367 | /// } 368 | /// ``` 369 | /// - Parameters: 370 | /// - type: Type of Associated object. 371 | /// - body: Contents of willSet 372 | /// - Returns: Variable that converts the contents of willSet to a closure 373 | static func `willSet`( 374 | type: TypeSyntax, 375 | accessor: AccessorDeclSyntax, 376 | body: CodeBlockSyntax 377 | ) -> VariableDeclSyntax { 378 | let newValue = accessor.parameters?.name.trimmed ?? .identifier("newValue") 379 | 380 | return VariableDeclSyntax( 381 | bindingSpecifier: .keyword(.let), 382 | bindings: .init() { 383 | .init( 384 | pattern: IdentifierPatternSyntax(identifier: .identifier("willSet")), 385 | typeAnnotation: .init( 386 | type: FunctionTypeSyntax( 387 | parameters: .init() { 388 | TupleTypeElementSyntax( 389 | type: type.trimmed 390 | ) 391 | }, 392 | returnClause: ReturnClauseSyntax( 393 | type: IdentifierTypeSyntax(name: .identifier("Void")) 394 | ) 395 | ) 396 | ), 397 | initializer: .init( 398 | value: ClosureExprSyntax( 399 | signature: .init( 400 | capture: .init() { 401 | #if canImport(SwiftSyntax601) 402 | ClosureCaptureSyntax( 403 | name: .keyword(.`self`) 404 | ) 405 | #else 406 | ClosureCaptureSyntax( 407 | expression: DeclReferenceExprSyntax( 408 | baseName: .keyword(.`self`) 409 | ) 410 | ) 411 | #endif 412 | }, 413 | parameterClause: .init(ClosureShorthandParameterListSyntax() { 414 | ClosureShorthandParameterSyntax(name: newValue) 415 | }) 416 | ), 417 | statements: .init(body.statements.map(\.trimmed)) 418 | ) 419 | ) 420 | ) 421 | } 422 | ) 423 | } 424 | 425 | /// `didSet` closure 426 | /// 427 | /// Convert a didSet accessor to a closure variable in the following format. 428 | /// ```swift 429 | /// let `didSet`: (\(type.trimmed)) -> Void = { [self] \(oldValue) in 430 | /// \(body.statements.trimmed) 431 | /// } 432 | /// ``` 433 | /// - Parameters: 434 | /// - type: Type of Associated object. 435 | /// - body: Contents of didSet 436 | /// - Returns: Variable that converts the contents of didSet to a closure 437 | static func `didSet`( 438 | type: TypeSyntax, 439 | accessor: AccessorDeclSyntax, 440 | body: CodeBlockSyntax 441 | ) -> VariableDeclSyntax { 442 | let oldValue = accessor.parameters?.name.trimmed ?? .identifier("oldValue") 443 | 444 | return VariableDeclSyntax( 445 | bindingSpecifier: .keyword(.let), 446 | bindings: .init() { 447 | .init( 448 | pattern: IdentifierPatternSyntax(identifier: .identifier("didSet")), 449 | typeAnnotation: .init( 450 | type: FunctionTypeSyntax( 451 | parameters: .init() { 452 | TupleTypeElementSyntax( 453 | type: type.trimmed 454 | ) 455 | }, 456 | returnClause: ReturnClauseSyntax( 457 | type: IdentifierTypeSyntax(name: .identifier("Void")) 458 | ) 459 | ) 460 | ), 461 | initializer: .init( 462 | value: ClosureExprSyntax( 463 | signature: .init( 464 | capture: .init() { 465 | #if canImport(SwiftSyntax601) 466 | ClosureCaptureSyntax( 467 | name: .keyword(.`self`) 468 | ) 469 | #else 470 | ClosureCaptureSyntax( 471 | expression: DeclReferenceExprSyntax( 472 | baseName: .keyword(.`self`) 473 | ) 474 | ) 475 | #endif 476 | }, 477 | parameterClause: .init(ClosureShorthandParameterListSyntax() { 478 | ClosureShorthandParameterSyntax(name: oldValue) 479 | }) 480 | ), 481 | statements: .init(body.statements.map(\.trimmed)) 482 | ) 483 | ) 484 | ) 485 | } 486 | ) 487 | } 488 | 489 | /// Execute willSet closure 490 | /// 491 | /// ```swift 492 | /// willSet(newValue) 493 | /// ``` 494 | /// - Returns: Syntax for executing willSet closure 495 | static func callWillSet() -> FunctionCallExprSyntax { 496 | FunctionCallExprSyntax( 497 | callee: DeclReferenceExprSyntax(baseName: .identifier("willSet")), 498 | argumentList: { 499 | .init( 500 | expression: DeclReferenceExprSyntax( 501 | baseName: .identifier("newValue") 502 | ) 503 | ) 504 | } 505 | ) 506 | } 507 | 508 | /// Execute didSet closure 509 | /// 510 | /// ```swift 511 | /// didSet(oldValue) 512 | /// ``` 513 | /// - Returns: Syntax for executing didSet closure 514 | static func callDidSet() -> FunctionCallExprSyntax { 515 | FunctionCallExprSyntax( 516 | callee: DeclReferenceExprSyntax(baseName: .identifier("didSet")), 517 | argumentList: { 518 | .init( 519 | expression: DeclReferenceExprSyntax( 520 | baseName: .identifier("oldValue") 521 | ) 522 | ) 523 | } 524 | ) 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /Sources/AssociatedObjectPlugin/AssociatedObjectMacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedObjectMacroDiagnostic.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/12. 6 | // 7 | // 8 | 9 | import SwiftSyntax 10 | import SwiftDiagnostics 11 | 12 | public enum AssociatedObjectMacroDiagnostic { 13 | case requiresVariableDeclaration 14 | case multipleVariableDeclarationIsNotSupported 15 | case getterAndSetterShouldBeNil 16 | case requiresInitialValue 17 | case specifyTypeExplicitly 18 | case invalidCustomKeySpecification 19 | } 20 | 21 | extension AssociatedObjectMacroDiagnostic: DiagnosticMessage { 22 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic { 23 | Diagnostic(node: Syntax(node), message: self) 24 | } 25 | 26 | public var message: String { 27 | switch self { 28 | case .requiresVariableDeclaration: 29 | return "`@AssociatedObject` must be attached to the property declaration." 30 | case .multipleVariableDeclarationIsNotSupported: 31 | return """ 32 | Multiple variable declaration in one statement is not supported when using `@AssociatedObject`. 33 | """ 34 | case .getterAndSetterShouldBeNil: 35 | return "getter and setter must not be implemented when using `@AssociatedObject`." 36 | case .requiresInitialValue: 37 | return "Initial values must be specified when using `@AssociatedObject`." 38 | case .specifyTypeExplicitly: 39 | return "Specify a type explicitly when using `@AssociatedObject`." 40 | case .invalidCustomKeySpecification: 41 | return "customKey specification is invalid." 42 | } 43 | } 44 | 45 | public var severity: DiagnosticSeverity { .error } 46 | 47 | public var diagnosticID: MessageID { 48 | MessageID(domain: "Swift", id: "AssociatedObject.\(self)") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/AssociatedObjectPlugin/AssociatedObjectMacrosPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedObjectMacrosPlugin.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/12. 6 | // 7 | // 8 | 9 | #if canImport(SwiftCompilerPlugin) 10 | import SwiftSyntaxMacros 11 | import SwiftCompilerPlugin 12 | 13 | @main 14 | struct AssociatedObjectMacrosPlugin: CompilerPlugin { 15 | let providingMacros: [Macro.Type] = [ 16 | AssociatedObjectMacro.self 17 | ] 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/AssociatedObjectPlugin/Extension/PatternBindingSyntax+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PatternBindingSyntax+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftSyntax 11 | 12 | extension PatternBindingSyntax { 13 | var setter: AccessorDeclSyntax? { 14 | get { 15 | guard let accessors = accessorBlock?.accessors, 16 | case let .accessors(list) = accessors else { 17 | return nil 18 | } 19 | return list.first(where: { 20 | $0.accessorSpecifier.tokenKind == .keyword(.set) 21 | }) 22 | } 23 | 24 | set { 25 | // NOTE: Be careful that setter cannot be implemented without a getter. 26 | setNewAccessor(kind: .keyword(.set), newValue: newValue) 27 | } 28 | } 29 | 30 | var getter: AccessorDeclSyntax? { 31 | get { 32 | switch accessorBlock?.accessors { 33 | case let .accessors(list): 34 | return list.first(where: { 35 | $0.accessorSpecifier.tokenKind == .keyword(.get) 36 | }) 37 | case let .getter(body): 38 | return AccessorDeclSyntax(accessorSpecifier: .keyword(.get), body: .init(statements: body)) 39 | case .none: 40 | return nil 41 | } 42 | } 43 | 44 | set { 45 | let newAccessors: AccessorBlockSyntax.Accessors 46 | 47 | switch accessorBlock?.accessors { 48 | case .getter, .none: 49 | if let newValue { 50 | if let body = newValue.body { 51 | newAccessors = .getter(body.statements) 52 | } else { 53 | let accessors = AccessorDeclListSyntax { 54 | newValue 55 | } 56 | newAccessors = .accessors(accessors) 57 | } 58 | } else { 59 | accessorBlock = .none 60 | return 61 | } 62 | 63 | case let .accessors(list): 64 | var newList = list 65 | let accessor = list.first(where: { accessor in 66 | accessor.accessorSpecifier.tokenKind == .keyword(.get) 67 | }) 68 | if let accessor, 69 | let index = list.index(of: accessor) { 70 | if let newValue { 71 | newList[index] = newValue 72 | } else { 73 | newList.remove(at: index) 74 | } 75 | } else if let newValue { 76 | newList.append(newValue) 77 | } 78 | newAccessors = .accessors(newList) 79 | } 80 | 81 | if accessorBlock == nil { 82 | accessorBlock = .init(accessors: newAccessors) 83 | } else { 84 | accessorBlock = accessorBlock?.with(\.accessors, newAccessors) 85 | } 86 | } 87 | } 88 | 89 | var isGetOnly: Bool { 90 | if initializer != nil { 91 | return false 92 | } 93 | if let accessors = accessorBlock?.accessors, 94 | case let .accessors(list) = accessors, 95 | list.contains(where: { $0.accessorSpecifier.tokenKind == .keyword(.set) }) { 96 | return false 97 | } 98 | if accessorBlock == nil && initializer == nil { 99 | return false 100 | } 101 | return true 102 | } 103 | } 104 | 105 | extension PatternBindingSyntax { 106 | var willSet: AccessorDeclSyntax? { 107 | get { 108 | if let accessors = accessorBlock?.accessors, 109 | case let .accessors(list) = accessors { 110 | return list.first(where: { 111 | $0.accessorSpecifier.tokenKind == .keyword(.willSet) 112 | }) 113 | } 114 | return nil 115 | } 116 | set { 117 | // NOTE: Be careful that willSet cannot be implemented without a setter. 118 | setNewAccessor(kind: .keyword(.willSet), newValue: newValue) 119 | } 120 | } 121 | 122 | var didSet: AccessorDeclSyntax? { 123 | get { 124 | if let accessors = accessorBlock?.accessors, 125 | case let .accessors(list) = accessors { 126 | return list.first(where: { 127 | $0.accessorSpecifier.tokenKind == .keyword(.didSet) 128 | }) 129 | } 130 | return nil 131 | } 132 | set { 133 | // NOTE: Be careful that didSet cannot be implemented without a setter. 134 | setNewAccessor(kind: .keyword(.willSet), newValue: newValue) 135 | } 136 | } 137 | } 138 | 139 | extension PatternBindingSyntax { 140 | // NOTE: - getter requires extra steps and should not be used. 141 | private mutating func setNewAccessor(kind: TokenKind, newValue: AccessorDeclSyntax?) { 142 | var newAccessor: AccessorBlockSyntax.Accessors 143 | 144 | switch accessorBlock?.accessors { 145 | case let .getter(body): 146 | guard let newValue else { return } 147 | newAccessor = .accessors( 148 | AccessorDeclListSyntax { 149 | AccessorDeclSyntax(accessorSpecifier: .keyword(.get), body: .init(statements: body)) 150 | newValue 151 | } 152 | ) 153 | case let .accessors(list): 154 | var newList = list 155 | let accessor = list.first(where: { accessor in 156 | accessor.accessorSpecifier.tokenKind == kind 157 | }) 158 | if let accessor, 159 | let index = list.index(of: accessor) { 160 | if let newValue { 161 | newList[index] = newValue 162 | } else { 163 | newList.remove(at: index) 164 | } 165 | } 166 | newAccessor = .accessors(newList) 167 | case .none: 168 | guard let newValue else { return } 169 | newAccessor = .accessors( 170 | AccessorDeclListSyntax { 171 | newValue 172 | } 173 | ) 174 | } 175 | 176 | if accessorBlock == nil { 177 | accessorBlock = .init(accessors: newAccessor) 178 | } else { 179 | accessorBlock = accessorBlock?.with(\.accessors, newAccessor) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Sources/AssociatedObjectPlugin/Extension/TypeSyntax+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeSyntax+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/27. 6 | // 7 | // 8 | 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | 12 | extension TypeSyntax { 13 | var isOptional: Bool { 14 | if self.is(OptionalTypeSyntax.self) || self.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) { 15 | return true 16 | } 17 | if let simpleType = self.as(IdentifierTypeSyntax.self), 18 | simpleType.name.trimmed.text == "Optional" { 19 | return true 20 | } 21 | return false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/AssociatedObjectTests/AssociatedObjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftSyntaxMacros 3 | import SwiftSyntaxMacrosTestSupport 4 | import MacroTesting 5 | 6 | // Note: 7 | // Prior to version 510, if an initial value was set in AccessorMacro, it was left in place and expanded. 8 | // Therefore, the following test will always fail when run on version 509. Therefore, the following tests are excluded from execution. 9 | #if canImport(AssociatedObjectPlugin) && canImport(SwiftSyntax510) 10 | @testable import AssociatedObjectPlugin 11 | @testable import AssociatedObject 12 | 13 | final class AssociatedObjectTests: XCTestCase { 14 | 15 | override func invokeTest() { 16 | withMacroTesting( 17 | // isRecording: true, 18 | macros: ["AssociatedObject": AssociatedObjectMacro.self] 19 | ) { 20 | super.invokeTest() 21 | } 22 | } 23 | 24 | func testString() throws { 25 | assertMacro { 26 | """ 27 | @AssociatedObject(.retain(.atomic)) 28 | var string: String = "text" 29 | """ 30 | } expansion: { 31 | """ 32 | var string: String { 33 | get { 34 | if let value = getAssociatedObject( 35 | self, 36 | Self.__associated_stringKey 37 | ) as? String { 38 | return value 39 | } else { 40 | let value: String = "text" 41 | setAssociatedObject( 42 | self, 43 | Self.__associated_stringKey, 44 | value, 45 | .retain(.atomic) 46 | ) 47 | return value 48 | } 49 | } 50 | set { 51 | setAssociatedObject( 52 | self, 53 | Self.__associated_stringKey, 54 | newValue, 55 | .retain(.atomic) 56 | ) 57 | } 58 | } 59 | 60 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 61 | let f: @convention(c) () -> Void = { 62 | } 63 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 64 | } 65 | """ 66 | } 67 | } 68 | 69 | func testInt() throws { 70 | assertMacro { 71 | """ 72 | @AssociatedObject(.retain(.nonatomic)) 73 | var int: Int = 5 74 | """ 75 | } expansion: { 76 | """ 77 | var int: Int { 78 | get { 79 | if let value = getAssociatedObject( 80 | self, 81 | Self.__associated_intKey 82 | ) as? Int { 83 | return value 84 | } else { 85 | let value: Int = 5 86 | setAssociatedObject( 87 | self, 88 | Self.__associated_intKey, 89 | value, 90 | .retain(.nonatomic) 91 | ) 92 | return value 93 | } 94 | } 95 | set { 96 | setAssociatedObject( 97 | self, 98 | Self.__associated_intKey, 99 | newValue, 100 | .retain(.nonatomic) 101 | ) 102 | } 103 | } 104 | 105 | @inline(never) static var __associated_intKey: UnsafeRawPointer { 106 | let f: @convention(c) () -> Void = { 107 | } 108 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 109 | } 110 | """ 111 | } 112 | } 113 | 114 | func testFloat() throws { 115 | assertMacro { 116 | """ 117 | @AssociatedObject(.retain(.nonatomic)) 118 | var float: Float = 5.0 119 | """ 120 | } expansion: { 121 | """ 122 | var float: Float { 123 | get { 124 | if let value = getAssociatedObject( 125 | self, 126 | Self.__associated_floatKey 127 | ) as? Float { 128 | return value 129 | } else { 130 | let value: Float = 5.0 131 | setAssociatedObject( 132 | self, 133 | Self.__associated_floatKey, 134 | value, 135 | .retain(.nonatomic) 136 | ) 137 | return value 138 | } 139 | } 140 | set { 141 | setAssociatedObject( 142 | self, 143 | Self.__associated_floatKey, 144 | newValue, 145 | .retain(.nonatomic) 146 | ) 147 | } 148 | } 149 | 150 | @inline(never) static var __associated_floatKey: UnsafeRawPointer { 151 | let f: @convention(c) () -> Void = { 152 | } 153 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 154 | } 155 | """ 156 | } 157 | } 158 | 159 | func testDouble() throws { 160 | assertMacro { 161 | """ 162 | @AssociatedObject(.retain(.nonatomic)) 163 | var double: Double = 5.0 164 | """ 165 | } expansion: { 166 | """ 167 | var double: Double { 168 | get { 169 | if let value = getAssociatedObject( 170 | self, 171 | Self.__associated_doubleKey 172 | ) as? Double { 173 | return value 174 | } else { 175 | let value: Double = 5.0 176 | setAssociatedObject( 177 | self, 178 | Self.__associated_doubleKey, 179 | value, 180 | .retain(.nonatomic) 181 | ) 182 | return value 183 | } 184 | } 185 | set { 186 | setAssociatedObject( 187 | self, 188 | Self.__associated_doubleKey, 189 | newValue, 190 | .retain(.nonatomic) 191 | ) 192 | } 193 | } 194 | 195 | @inline(never) static var __associated_doubleKey: UnsafeRawPointer { 196 | let f: @convention(c) () -> Void = { 197 | } 198 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 199 | } 200 | """ 201 | } 202 | } 203 | 204 | func testStringWithOtherPolicy() throws { 205 | assertMacro { 206 | """ 207 | @AssociatedObject(.retain(.nonatomic)) 208 | var string: String = "text" 209 | """ 210 | } expansion: { 211 | """ 212 | var string: String { 213 | get { 214 | if let value = getAssociatedObject( 215 | self, 216 | Self.__associated_stringKey 217 | ) as? String { 218 | return value 219 | } else { 220 | let value: String = "text" 221 | setAssociatedObject( 222 | self, 223 | Self.__associated_stringKey, 224 | value, 225 | .retain(.nonatomic) 226 | ) 227 | return value 228 | } 229 | } 230 | set { 231 | setAssociatedObject( 232 | self, 233 | Self.__associated_stringKey, 234 | newValue, 235 | .retain(.nonatomic) 236 | ) 237 | } 238 | } 239 | 240 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 241 | let f: @convention(c) () -> Void = { 242 | } 243 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 244 | } 245 | """ 246 | } 247 | } 248 | 249 | func testOptionalString() throws { 250 | assertMacro { 251 | """ 252 | @AssociatedObject(.retain(.nonatomic)) 253 | var string: String? 254 | """ 255 | } expansion: { 256 | """ 257 | var string: String? { 258 | get { 259 | getAssociatedObject( 260 | self, 261 | Self.__associated_stringKey 262 | ) as? String 263 | ?? nil 264 | } 265 | set { 266 | setAssociatedObject( 267 | self, 268 | Self.__associated_stringKey, 269 | newValue, 270 | .retain(.nonatomic) 271 | ) 272 | } 273 | } 274 | 275 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 276 | let f: @convention(c) () -> Void = { 277 | } 278 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 279 | } 280 | """ 281 | } 282 | } 283 | 284 | func testOptionalGenericsString() throws { 285 | assertMacro { 286 | """ 287 | @AssociatedObject(.retain(.nonatomic)) 288 | var string: Optional 289 | """ 290 | } expansion: { 291 | """ 292 | var string: Optional { 293 | get { 294 | getAssociatedObject( 295 | self, 296 | Self.__associated_stringKey 297 | ) as? Optional 298 | ?? nil 299 | } 300 | set { 301 | setAssociatedObject( 302 | self, 303 | Self.__associated_stringKey, 304 | newValue, 305 | .retain(.nonatomic) 306 | ) 307 | } 308 | } 309 | 310 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 311 | let f: @convention(c) () -> Void = { 312 | } 313 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 314 | } 315 | """ 316 | } 317 | } 318 | 319 | func testImplicitlyUnwrappedOptionalString() throws { 320 | assertMacro { 321 | """ 322 | @AssociatedObject(.retain(.nonatomic)) 323 | var string: String! 324 | """ 325 | } expansion: { 326 | """ 327 | var string: String! { 328 | get { 329 | getAssociatedObject( 330 | self, 331 | Self.__associated_stringKey 332 | ) as? String 333 | ?? nil 334 | } 335 | set { 336 | setAssociatedObject( 337 | self, 338 | Self.__associated_stringKey, 339 | newValue, 340 | .retain(.nonatomic) 341 | ) 342 | } 343 | } 344 | 345 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 346 | let f: @convention(c) () -> Void = { 347 | } 348 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 349 | } 350 | """ 351 | } 352 | } 353 | 354 | func testOptionalStringWithInitialValue() throws { 355 | assertMacro { 356 | """ 357 | @AssociatedObject(.retain(.nonatomic)) 358 | var string: String? = "hello" 359 | """ 360 | } expansion: { 361 | """ 362 | var string: String? { 363 | get { 364 | if !self.__associated_stringIsSet { 365 | let value: String? = "hello" 366 | setAssociatedObject( 367 | self, 368 | Self.__associated_stringKey, 369 | value, 370 | .retain(.nonatomic) 371 | ) 372 | self.__associated_stringIsSet = true 373 | return value 374 | } else { 375 | return getAssociatedObject( 376 | self, 377 | Self.__associated_stringKey 378 | ) as? String 379 | } 380 | } 381 | set { 382 | setAssociatedObject( 383 | self, 384 | Self.__associated_stringKey, 385 | newValue, 386 | .retain(.nonatomic) 387 | ) 388 | self.__associated_stringIsSet = true 389 | } 390 | } 391 | 392 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 393 | let f: @convention(c) () -> Void = { 394 | } 395 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 396 | } 397 | 398 | @_AssociatedObject(.retain(.nonatomic)) var __associated_stringIsSet: Bool = false 399 | 400 | @inline(never) static var __associated___associated_stringIsSetKey: UnsafeRawPointer { 401 | let f: @convention(c) () -> Void = { 402 | } 403 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 404 | } 405 | """ 406 | } 407 | } 408 | 409 | func testBool() throws { 410 | assertMacro { 411 | """ 412 | @AssociatedObject(.retain(.nonatomic)) 413 | var bool: Bool = false 414 | """ 415 | } expansion: { 416 | """ 417 | var bool: Bool { 418 | get { 419 | if let value = getAssociatedObject( 420 | self, 421 | Self.__associated_boolKey 422 | ) as? Bool { 423 | return value 424 | } else { 425 | let value: Bool = false 426 | setAssociatedObject( 427 | self, 428 | Self.__associated_boolKey, 429 | value, 430 | .retain(.nonatomic) 431 | ) 432 | return value 433 | } 434 | } 435 | set { 436 | setAssociatedObject( 437 | self, 438 | Self.__associated_boolKey, 439 | newValue, 440 | .retain(.nonatomic) 441 | ) 442 | } 443 | } 444 | 445 | @inline(never) static var __associated_boolKey: UnsafeRawPointer { 446 | let f: @convention(c) () -> Void = { 447 | } 448 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 449 | } 450 | """ 451 | } 452 | } 453 | 454 | func testIntArray() throws { 455 | assertMacro { 456 | """ 457 | @AssociatedObject(.retain(.nonatomic)) 458 | var intArray: [Int] = [1, 2, 3] 459 | """ 460 | } expansion: { 461 | """ 462 | var intArray: [Int] { 463 | get { 464 | if let value = getAssociatedObject( 465 | self, 466 | Self.__associated_intArrayKey 467 | ) as? [Int] { 468 | return value 469 | } else { 470 | let value: [Int] = [1, 2, 3] 471 | setAssociatedObject( 472 | self, 473 | Self.__associated_intArrayKey, 474 | value, 475 | .retain(.nonatomic) 476 | ) 477 | return value 478 | } 479 | } 480 | set { 481 | setAssociatedObject( 482 | self, 483 | Self.__associated_intArrayKey, 484 | newValue, 485 | .retain(.nonatomic) 486 | ) 487 | } 488 | } 489 | 490 | @inline(never) static var __associated_intArrayKey: UnsafeRawPointer { 491 | let f: @convention(c) () -> Void = { 492 | } 493 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 494 | } 495 | """ 496 | } 497 | } 498 | 499 | func testOptionalBool() throws { 500 | assertMacro { 501 | """ 502 | @AssociatedObject(.retain(.nonatomic)) 503 | var bool: Bool? 504 | """ 505 | } expansion: { 506 | """ 507 | var bool: Bool? { 508 | get { 509 | getAssociatedObject( 510 | self, 511 | Self.__associated_boolKey 512 | ) as? Bool 513 | ?? nil 514 | } 515 | set { 516 | setAssociatedObject( 517 | self, 518 | Self.__associated_boolKey, 519 | newValue, 520 | .retain(.nonatomic) 521 | ) 522 | } 523 | } 524 | 525 | @inline(never) static var __associated_boolKey: UnsafeRawPointer { 526 | let f: @convention(c) () -> Void = { 527 | } 528 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 529 | } 530 | """ 531 | } 532 | } 533 | 534 | func testDictionary() throws { 535 | assertMacro { 536 | """ 537 | @AssociatedObject(.retain(.nonatomic)) 538 | var dic: [String: String] = ["t": "a"] 539 | """ 540 | } expansion: { 541 | """ 542 | var dic: [String: String] { 543 | get { 544 | if let value = getAssociatedObject( 545 | self, 546 | Self.__associated_dicKey 547 | ) as? [String: String] { 548 | return value 549 | } else { 550 | let value: [String: String] = ["t": "a"] 551 | setAssociatedObject( 552 | self, 553 | Self.__associated_dicKey, 554 | value, 555 | .retain(.nonatomic) 556 | ) 557 | return value 558 | } 559 | } 560 | set { 561 | setAssociatedObject( 562 | self, 563 | Self.__associated_dicKey, 564 | newValue, 565 | .retain(.nonatomic) 566 | ) 567 | } 568 | } 569 | 570 | @inline(never) static var __associated_dicKey: UnsafeRawPointer { 571 | let f: @convention(c) () -> Void = { 572 | } 573 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 574 | } 575 | """ 576 | } 577 | } 578 | 579 | func testWillSet() throws { 580 | assertMacro { 581 | """ 582 | @AssociatedObject(.retain(.nonatomic)) 583 | var string: String = "text" { 584 | willSet { 585 | print("willSet: old", string) 586 | print("willSet: new", newValue) 587 | } 588 | } 589 | """ 590 | } expansion: { 591 | """ 592 | var string: String { 593 | willSet { 594 | print("willSet: old", string) 595 | print("willSet: new", newValue) 596 | } 597 | get { 598 | if let value = getAssociatedObject( 599 | self, 600 | Self.__associated_stringKey 601 | ) as? String { 602 | return value 603 | } else { 604 | let value: String = "text" 605 | setAssociatedObject( 606 | self, 607 | Self.__associated_stringKey, 608 | value, 609 | .retain(.nonatomic) 610 | ) 611 | return value 612 | } 613 | } 614 | 615 | set { 616 | let willSet: (String) -> Void = { [self] newValue in 617 | print("willSet: old", string) 618 | print("willSet: new", newValue) 619 | } 620 | willSet(newValue) 621 | 622 | setAssociatedObject( 623 | self, 624 | Self.__associated_stringKey, 625 | newValue, 626 | .retain(.nonatomic) 627 | ) 628 | } 629 | } 630 | 631 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 632 | let f: @convention(c) () -> Void = { 633 | } 634 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 635 | } 636 | """ 637 | } 638 | } 639 | 640 | func testDidSet() throws { 641 | assertMacro { 642 | """ 643 | @AssociatedObject(.retain(.nonatomic)) 644 | var string: String = "text" { 645 | didSet { 646 | print("didSet: old", oldValue) 647 | } 648 | } 649 | """ 650 | } expansion: { 651 | """ 652 | var string: String { 653 | didSet { 654 | print("didSet: old", oldValue) 655 | } 656 | get { 657 | if let value = getAssociatedObject( 658 | self, 659 | Self.__associated_stringKey 660 | ) as? String { 661 | return value 662 | } else { 663 | let value: String = "text" 664 | setAssociatedObject( 665 | self, 666 | Self.__associated_stringKey, 667 | value, 668 | .retain(.nonatomic) 669 | ) 670 | return value 671 | } 672 | } 673 | 674 | set { 675 | let oldValue = string 676 | setAssociatedObject( 677 | self, 678 | Self.__associated_stringKey, 679 | newValue, 680 | .retain(.nonatomic) 681 | ) 682 | 683 | let didSet: (String) -> Void = { [self] oldValue in 684 | print("didSet: old", oldValue) 685 | } 686 | didSet(oldValue) 687 | } 688 | } 689 | 690 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 691 | let f: @convention(c) () -> Void = { 692 | } 693 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 694 | } 695 | """ 696 | } 697 | } 698 | 699 | func testWillSetAndDidSet() throws { 700 | assertMacro { 701 | """ 702 | @AssociatedObject(.retain(.nonatomic)) 703 | var string: String = "text" { 704 | willSet { 705 | print("willSet: old", string) 706 | print("willSet: new", newValue) 707 | } 708 | didSet { 709 | print("didSet: old", oldValue) 710 | } 711 | } 712 | """ 713 | } expansion: { 714 | """ 715 | var string: String { 716 | willSet { 717 | print("willSet: old", string) 718 | print("willSet: new", newValue) 719 | } 720 | didSet { 721 | print("didSet: old", oldValue) 722 | } 723 | get { 724 | if let value = getAssociatedObject( 725 | self, 726 | Self.__associated_stringKey 727 | ) as? String { 728 | return value 729 | } else { 730 | let value: String = "text" 731 | setAssociatedObject( 732 | self, 733 | Self.__associated_stringKey, 734 | value, 735 | .retain(.nonatomic) 736 | ) 737 | return value 738 | } 739 | } 740 | 741 | set { 742 | let willSet: (String) -> Void = { [self] newValue in 743 | print("willSet: old", string) 744 | print("willSet: new", newValue) 745 | } 746 | willSet(newValue) 747 | 748 | let oldValue = string 749 | setAssociatedObject( 750 | self, 751 | Self.__associated_stringKey, 752 | newValue, 753 | .retain(.nonatomic) 754 | ) 755 | 756 | let didSet: (String) -> Void = { [self] oldValue in 757 | print("didSet: old", oldValue) 758 | } 759 | didSet(oldValue) 760 | } 761 | } 762 | 763 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 764 | let f: @convention(c) () -> Void = { 765 | } 766 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 767 | } 768 | """ 769 | } 770 | } 771 | 772 | func testWillSetWithArgument() throws { 773 | assertMacro { 774 | """ 775 | @AssociatedObject(.retain(.nonatomic)) 776 | var string: String = "text" { 777 | willSet(new) { 778 | print("willSet: old", string) 779 | print("willSet: new", new) 780 | } 781 | } 782 | """ 783 | } expansion: { 784 | """ 785 | var string: String { 786 | willSet(new) { 787 | print("willSet: old", string) 788 | print("willSet: new", new) 789 | } 790 | get { 791 | if let value = getAssociatedObject( 792 | self, 793 | Self.__associated_stringKey 794 | ) as? String { 795 | return value 796 | } else { 797 | let value: String = "text" 798 | setAssociatedObject( 799 | self, 800 | Self.__associated_stringKey, 801 | value, 802 | .retain(.nonatomic) 803 | ) 804 | return value 805 | } 806 | } 807 | 808 | set { 809 | let willSet: (String) -> Void = { [self] new in 810 | print("willSet: old", string) 811 | print("willSet: new", new) 812 | } 813 | willSet(newValue) 814 | 815 | setAssociatedObject( 816 | self, 817 | Self.__associated_stringKey, 818 | newValue, 819 | .retain(.nonatomic) 820 | ) 821 | } 822 | } 823 | 824 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 825 | let f: @convention(c) () -> Void = { 826 | } 827 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 828 | } 829 | """ 830 | } 831 | } 832 | 833 | func testDidSetWithArgument() throws { 834 | assertMacro { 835 | """ 836 | @AssociatedObject(.retain(.nonatomic)) 837 | var string: String = "text" { 838 | didSet(old) { 839 | print("didSet: old", old) 840 | } 841 | } 842 | """ 843 | } expansion: { 844 | """ 845 | var string: String { 846 | didSet(old) { 847 | print("didSet: old", old) 848 | } 849 | get { 850 | if let value = getAssociatedObject( 851 | self, 852 | Self.__associated_stringKey 853 | ) as? String { 854 | return value 855 | } else { 856 | let value: String = "text" 857 | setAssociatedObject( 858 | self, 859 | Self.__associated_stringKey, 860 | value, 861 | .retain(.nonatomic) 862 | ) 863 | return value 864 | } 865 | } 866 | 867 | set { 868 | let oldValue = string 869 | setAssociatedObject( 870 | self, 871 | Self.__associated_stringKey, 872 | newValue, 873 | .retain(.nonatomic) 874 | ) 875 | 876 | let didSet: (String) -> Void = { [self] old in 877 | print("didSet: old", old) 878 | } 879 | didSet(oldValue) 880 | } 881 | } 882 | 883 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 884 | let f: @convention(c) () -> Void = { 885 | } 886 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 887 | } 888 | """ 889 | } 890 | } 891 | 892 | func testModernWritingStyle() throws { 893 | assertMacro { 894 | """ 895 | @AssociatedObject(.copy(.nonatomic)) 896 | var string: String = "text" 897 | """ 898 | } expansion: { 899 | """ 900 | var string: String { 901 | get { 902 | if let value = getAssociatedObject( 903 | self, 904 | Self.__associated_stringKey 905 | ) as? String { 906 | return value 907 | } else { 908 | let value: String = "text" 909 | setAssociatedObject( 910 | self, 911 | Self.__associated_stringKey, 912 | value, 913 | .copy(.nonatomic) 914 | ) 915 | return value 916 | } 917 | } 918 | set { 919 | setAssociatedObject( 920 | self, 921 | Self.__associated_stringKey, 922 | newValue, 923 | .copy(.nonatomic) 924 | ) 925 | } 926 | } 927 | 928 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 929 | let f: @convention(c) () -> Void = { 930 | } 931 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 932 | } 933 | """ 934 | } 935 | } 936 | 937 | // MARK: Diagnostics test 938 | func testDiagnosticsDeclarationType() throws { 939 | assertMacro { 940 | """ 941 | @AssociatedObject(.retain(.nonatomic)) 942 | struct Item {} 943 | """ 944 | } diagnostics: { 945 | """ 946 | @AssociatedObject(.retain(.nonatomic)) 947 | ╰─ 🛑 `@AssociatedObject` must be attached to the property declaration. 948 | struct Item {} 949 | """ 950 | } 951 | } 952 | 953 | func testDiagnosticsGetterAndSetter() throws { 954 | assertMacro { 955 | """ 956 | @AssociatedObject(.retain(.nonatomic)) 957 | var string: String? { 958 | get { "" } 959 | set {} 960 | } 961 | """ 962 | } diagnostics: { 963 | """ 964 | @AssociatedObject(.retain(.nonatomic)) 965 | var string: String? { 966 | get { "" } 967 | set {} 968 | ┬───── 969 | ╰─ 🛑 getter and setter must not be implemented when using `@AssociatedObject`. 970 | } 971 | """ 972 | } 973 | } 974 | 975 | func testDiagnosticsInitialValue() throws { 976 | assertMacro { 977 | """ 978 | @AssociatedObject(.retain(.nonatomic)) 979 | var string: String 980 | """ 981 | } diagnostics: { 982 | """ 983 | @AssociatedObject(.retain(.nonatomic)) 984 | ╰─ 🛑 Initial values must be specified when using `@AssociatedObject`. 985 | var string: String 986 | """ 987 | } 988 | } 989 | 990 | func testDiagnosticsSpecifyType() throws { 991 | assertMacro { 992 | """ 993 | @AssociatedObject(.retain(.nonatomic)) 994 | var string = ["text", 123] 995 | """ 996 | } diagnostics: { 997 | """ 998 | @AssociatedObject(.retain(.nonatomic)) 999 | var string = ["text", 123] 1000 | ┬───── 1001 | ├─ 🛑 Specify a type explicitly when using `@AssociatedObject`. 1002 | ╰─ 🛑 Specify a type explicitly when using `@AssociatedObject`. 1003 | """ 1004 | } 1005 | } 1006 | 1007 | func testDiagnosticsInvalidCustomKey() throws { 1008 | assertMacro { 1009 | """ 1010 | @AssociatedObject(.retain(.nonatomic), key: "key") 1011 | var string = "string" 1012 | """ 1013 | } diagnostics: { 1014 | """ 1015 | @AssociatedObject(.retain(.nonatomic), key: "key") 1016 | ┬───────── 1017 | ╰─ 🛑 customKey specification is invalid. 1018 | var string = "string" 1019 | """ 1020 | } 1021 | } 1022 | } 1023 | 1024 | extension AssociatedObjectTests { 1025 | func testCustomStoreKey() throws { 1026 | assertMacro { 1027 | """ 1028 | @AssociatedObject(.retain(.nonatomic), key: key) 1029 | var string: String = "text" 1030 | """ 1031 | } expansion: { 1032 | """ 1033 | var string: String { 1034 | get { 1035 | if let value = getAssociatedObject( 1036 | self, 1037 | &key 1038 | ) as? String { 1039 | return value 1040 | } else { 1041 | let value: String = "text" 1042 | setAssociatedObject( 1043 | self, 1044 | &key, 1045 | value, 1046 | .retain(.nonatomic) 1047 | ) 1048 | return value 1049 | } 1050 | } 1051 | set { 1052 | setAssociatedObject( 1053 | self, 1054 | &key, 1055 | newValue, 1056 | .retain(.nonatomic) 1057 | ) 1058 | } 1059 | } 1060 | """ 1061 | } 1062 | } 1063 | } 1064 | 1065 | extension AssociatedObjectTests { 1066 | func testOptional() { 1067 | let item = ClassType() 1068 | XCTAssertEqual(item.optionalDouble, 123.4) 1069 | 1070 | item.optionalDouble = nil 1071 | XCTAssertEqual(item.optionalDouble, nil) 1072 | 1073 | item.implicitlyUnwrappedString = "hello" 1074 | XCTAssertEqual(item.implicitlyUnwrappedString, "hello") 1075 | 1076 | item.implicitlyUnwrappedString = nil 1077 | XCTAssertEqual(item.implicitlyUnwrappedString, nil) 1078 | 1079 | item.implicitlyUnwrappedString = "modified hello" 1080 | XCTAssertEqual(item.implicitlyUnwrappedString, "modified hello") 1081 | } 1082 | 1083 | func testSetDefaultValue() { 1084 | let item = ClassType() 1085 | XCTAssertTrue(item.classType === item.classType) 1086 | } 1087 | 1088 | func testProtocol() { 1089 | let item = ClassType() 1090 | XCTAssertEqual(item.definedInProtocol, "hello") 1091 | 1092 | item.definedInProtocol = "modified" 1093 | XCTAssertEqual(item.definedInProtocol, "modified") 1094 | } 1095 | } 1096 | 1097 | extension AssociatedObjectTests { 1098 | func testKeysUnique() { 1099 | let keys = [ 1100 | ClassType.__associated_intKey, 1101 | ClassType.__associated_doubleKey, 1102 | ClassType.__associated_stringKey, 1103 | ClassType.__associated_boolKey, 1104 | ClassType.__associated_optionalIntKey, 1105 | ClassType.__associated_optionalDoubleKey, 1106 | ClassType.__associated_optionalStringKey, 1107 | ClassType.__associated_optionalBoolKey, 1108 | ClassType.__associated_implicitlyUnwrappedStringKey, 1109 | ClassType.__associated_intArrayKey, 1110 | ClassType.__associated_doubleArrayKey, 1111 | ClassType.__associated_stringArrayKey, 1112 | ClassType.__associated_boolArrayKey, 1113 | ClassType.__associated_optionalIntArrayKey, 1114 | ClassType.__associated_optionalDoubleArrayKey, 1115 | ClassType.__associated_optionalStringArrayKey, 1116 | ClassType.__associated_optionalBoolArrayKey, 1117 | ClassType.__associated_classTypeKey, 1118 | ] 1119 | XCTAssertEqual(Set(keys).count, keys.count) 1120 | } 1121 | } 1122 | #endif 1123 | -------------------------------------------------------------------------------- /Tests/AssociatedObjectTests/AssociatedTypeDetectionObjectTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedTypeDetectionObjectTests.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/01/18. 6 | // 7 | // 8 | 9 | import XCTest 10 | import SwiftSyntaxMacros 11 | import SwiftSyntaxMacrosTestSupport 12 | import MacroTesting 13 | 14 | #if canImport(AssociatedObjectPlugin) && canImport(SwiftSyntax510) 15 | @testable import AssociatedObjectPlugin 16 | @testable import AssociatedObject 17 | 18 | // MARK: - TypeDetection 19 | // thanks: https://github.com/mlch911 20 | final class AssociatedTypeDetectionObjectTests: XCTestCase { 21 | override func invokeTest() { 22 | withMacroTesting( 23 | // isRecording: true, 24 | macros: ["AssociatedObject": AssociatedObjectMacro.self] 25 | ) { 26 | super.invokeTest() 27 | } 28 | } 29 | } 30 | 31 | // MARK: - Simple Types 32 | extension AssociatedTypeDetectionObjectTests { 33 | func testTypeDetectionInt() throws { 34 | assertMacro { 35 | """ 36 | @AssociatedObject(.retain(.nonatomic)) 37 | var int = 10 38 | """ 39 | } expansion: { 40 | """ 41 | var int { 42 | get { 43 | if let value = getAssociatedObject( 44 | self, 45 | Self.__associated_intKey 46 | ) as? Swift.Int { 47 | return value 48 | } else { 49 | let value: Swift.Int = 10 50 | setAssociatedObject( 51 | self, 52 | Self.__associated_intKey, 53 | value, 54 | .retain(.nonatomic) 55 | ) 56 | return value 57 | } 58 | } 59 | set { 60 | setAssociatedObject( 61 | self, 62 | Self.__associated_intKey, 63 | newValue, 64 | .retain(.nonatomic) 65 | ) 66 | } 67 | } 68 | 69 | @inline(never) static var __associated_intKey: UnsafeRawPointer { 70 | let f: @convention(c) () -> Void = { 71 | } 72 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 73 | } 74 | """ 75 | } 76 | } 77 | 78 | func testTypeDetectionDouble() throws { 79 | assertMacro { 80 | """ 81 | @AssociatedObject(.retain(.nonatomic)) 82 | var double = 10.0 83 | """ 84 | } expansion: { 85 | """ 86 | var double { 87 | get { 88 | if let value = getAssociatedObject( 89 | self, 90 | Self.__associated_doubleKey 91 | ) as? Swift.Double { 92 | return value 93 | } else { 94 | let value: Swift.Double = 10.0 95 | setAssociatedObject( 96 | self, 97 | Self.__associated_doubleKey, 98 | value, 99 | .retain(.nonatomic) 100 | ) 101 | return value 102 | } 103 | } 104 | set { 105 | setAssociatedObject( 106 | self, 107 | Self.__associated_doubleKey, 108 | newValue, 109 | .retain(.nonatomic) 110 | ) 111 | } 112 | } 113 | 114 | @inline(never) static var __associated_doubleKey: UnsafeRawPointer { 115 | let f: @convention(c) () -> Void = { 116 | } 117 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 118 | } 119 | """ 120 | } 121 | } 122 | 123 | func testTypeDetectionString() throws { 124 | assertMacro { 125 | """ 126 | @AssociatedObject(.retain(.nonatomic)) 127 | var string = "text" 128 | """ 129 | } expansion: { 130 | """ 131 | var string { 132 | get { 133 | if let value = getAssociatedObject( 134 | self, 135 | Self.__associated_stringKey 136 | ) as? Swift.String { 137 | return value 138 | } else { 139 | let value: Swift.String = "text" 140 | setAssociatedObject( 141 | self, 142 | Self.__associated_stringKey, 143 | value, 144 | .retain(.nonatomic) 145 | ) 146 | return value 147 | } 148 | } 149 | set { 150 | setAssociatedObject( 151 | self, 152 | Self.__associated_stringKey, 153 | newValue, 154 | .retain(.nonatomic) 155 | ) 156 | } 157 | } 158 | 159 | @inline(never) static var __associated_stringKey: UnsafeRawPointer { 160 | let f: @convention(c) () -> Void = { 161 | } 162 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 163 | } 164 | """ 165 | } 166 | } 167 | 168 | func testTypeDetectionBool() throws { 169 | assertMacro { 170 | """ 171 | @AssociatedObject(.retain(.nonatomic)) 172 | var bool = false 173 | """ 174 | } expansion: { 175 | """ 176 | var bool { 177 | get { 178 | if let value = getAssociatedObject( 179 | self, 180 | Self.__associated_boolKey 181 | ) as? Swift.Bool { 182 | return value 183 | } else { 184 | let value: Swift.Bool = false 185 | setAssociatedObject( 186 | self, 187 | Self.__associated_boolKey, 188 | value, 189 | .retain(.nonatomic) 190 | ) 191 | return value 192 | } 193 | } 194 | set { 195 | setAssociatedObject( 196 | self, 197 | Self.__associated_boolKey, 198 | newValue, 199 | .retain(.nonatomic) 200 | ) 201 | } 202 | } 203 | 204 | @inline(never) static var __associated_boolKey: UnsafeRawPointer { 205 | let f: @convention(c) () -> Void = { 206 | } 207 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 208 | } 209 | """ 210 | } 211 | } 212 | } 213 | 214 | // MARK: - Array 215 | extension AssociatedTypeDetectionObjectTests { 216 | func testTypeDetectionIntArray() throws { 217 | assertMacro { 218 | """ 219 | @AssociatedObject(.retain(.nonatomic)) 220 | var intArray = [1, 2, 3] 221 | """ 222 | } expansion: { 223 | """ 224 | var intArray { 225 | get { 226 | if let value = getAssociatedObject( 227 | self, 228 | Self.__associated_intArrayKey 229 | ) as? [Swift.Int] { 230 | return value 231 | } else { 232 | let value: [Swift.Int] = [1, 2, 3] 233 | setAssociatedObject( 234 | self, 235 | Self.__associated_intArrayKey, 236 | value, 237 | .retain(.nonatomic) 238 | ) 239 | return value 240 | } 241 | } 242 | set { 243 | setAssociatedObject( 244 | self, 245 | Self.__associated_intArrayKey, 246 | newValue, 247 | .retain(.nonatomic) 248 | ) 249 | } 250 | } 251 | 252 | @inline(never) static var __associated_intArrayKey: UnsafeRawPointer { 253 | let f: @convention(c) () -> Void = { 254 | } 255 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 256 | } 257 | """ 258 | } 259 | } 260 | 261 | func testTypeDetectionDoubleArray() throws { 262 | assertMacro { 263 | """ 264 | @AssociatedObject(.retain(.nonatomic)) 265 | var doubleArray = [1.0, 2.0, 3.0] 266 | """ 267 | } expansion: { 268 | """ 269 | var doubleArray { 270 | get { 271 | if let value = getAssociatedObject( 272 | self, 273 | Self.__associated_doubleArrayKey 274 | ) as? [Swift.Double] { 275 | return value 276 | } else { 277 | let value: [Swift.Double] = [1.0, 2.0, 3.0] 278 | setAssociatedObject( 279 | self, 280 | Self.__associated_doubleArrayKey, 281 | value, 282 | .retain(.nonatomic) 283 | ) 284 | return value 285 | } 286 | } 287 | set { 288 | setAssociatedObject( 289 | self, 290 | Self.__associated_doubleArrayKey, 291 | newValue, 292 | .retain(.nonatomic) 293 | ) 294 | } 295 | } 296 | 297 | @inline(never) static var __associated_doubleArrayKey: UnsafeRawPointer { 298 | let f: @convention(c) () -> Void = { 299 | } 300 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 301 | } 302 | """ 303 | } 304 | } 305 | 306 | func testTypeDetectionIntAndDoubleArray() throws { 307 | assertMacro { 308 | """ 309 | @AssociatedObject(.retain(.nonatomic)) 310 | var doubleArray = [1, 1.0, 2, 2.0, 3, 3.0] 311 | """ 312 | } expansion: { 313 | """ 314 | var doubleArray { 315 | get { 316 | if let value = getAssociatedObject( 317 | self, 318 | Self.__associated_doubleArrayKey 319 | ) as? [Swift.Double] { 320 | return value 321 | } else { 322 | let value: [Swift.Double] = [1, 1.0, 2, 2.0, 3, 3.0] 323 | setAssociatedObject( 324 | self, 325 | Self.__associated_doubleArrayKey, 326 | value, 327 | .retain(.nonatomic) 328 | ) 329 | return value 330 | } 331 | } 332 | set { 333 | setAssociatedObject( 334 | self, 335 | Self.__associated_doubleArrayKey, 336 | newValue, 337 | .retain(.nonatomic) 338 | ) 339 | } 340 | } 341 | 342 | @inline(never) static var __associated_doubleArrayKey: UnsafeRawPointer { 343 | let f: @convention(c) () -> Void = { 344 | } 345 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 346 | } 347 | """ 348 | } 349 | } 350 | 351 | func testTypeDetectionBoolArray() throws { 352 | assertMacro { 353 | """ 354 | @AssociatedObject(.retain(.nonatomic)) 355 | var boolArray = [true, false] 356 | """ 357 | } expansion: { 358 | """ 359 | var boolArray { 360 | get { 361 | if let value = getAssociatedObject( 362 | self, 363 | Self.__associated_boolArrayKey 364 | ) as? [Swift.Bool] { 365 | return value 366 | } else { 367 | let value: [Swift.Bool] = [true, false] 368 | setAssociatedObject( 369 | self, 370 | Self.__associated_boolArrayKey, 371 | value, 372 | .retain(.nonatomic) 373 | ) 374 | return value 375 | } 376 | } 377 | set { 378 | setAssociatedObject( 379 | self, 380 | Self.__associated_boolArrayKey, 381 | newValue, 382 | .retain(.nonatomic) 383 | ) 384 | } 385 | } 386 | 387 | @inline(never) static var __associated_boolArrayKey: UnsafeRawPointer { 388 | let f: @convention(c) () -> Void = { 389 | } 390 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 391 | } 392 | """ 393 | } 394 | } 395 | 396 | func testTypeDetectionStringArray() throws { 397 | assertMacro { 398 | """ 399 | @AssociatedObject(.retain(.nonatomic)) 400 | var stringArray = ["1.0", "2.0", "3.0"] 401 | """ 402 | } expansion: { 403 | """ 404 | var stringArray { 405 | get { 406 | if let value = getAssociatedObject( 407 | self, 408 | Self.__associated_stringArrayKey 409 | ) as? [Swift.String] { 410 | return value 411 | } else { 412 | let value: [Swift.String] = ["1.0", "2.0", "3.0"] 413 | setAssociatedObject( 414 | self, 415 | Self.__associated_stringArrayKey, 416 | value, 417 | .retain(.nonatomic) 418 | ) 419 | return value 420 | } 421 | } 422 | set { 423 | setAssociatedObject( 424 | self, 425 | Self.__associated_stringArrayKey, 426 | newValue, 427 | .retain(.nonatomic) 428 | ) 429 | } 430 | } 431 | 432 | @inline(never) static var __associated_stringArrayKey: UnsafeRawPointer { 433 | let f: @convention(c) () -> Void = { 434 | } 435 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 436 | } 437 | """ 438 | } 439 | } 440 | } 441 | 442 | // MARK: - Array with optional Elements 443 | extension AssociatedTypeDetectionObjectTests { 444 | func testTypeDetectionOptionalIntArray() throws { 445 | assertMacro { 446 | """ 447 | @AssociatedObject(.retain(.nonatomic)) 448 | var optionalIntArray = [1, 1, nil, 2, 3, nil] 449 | """ 450 | } expansion: { 451 | """ 452 | var optionalIntArray { 453 | get { 454 | if let value = getAssociatedObject( 455 | self, 456 | Self.__associated_optionalIntArrayKey 457 | ) as? [Swift.Int?] { 458 | return value 459 | } else { 460 | let value: [Swift.Int?] = [1, 1, nil, 2, 3, nil] 461 | setAssociatedObject( 462 | self, 463 | Self.__associated_optionalIntArrayKey, 464 | value, 465 | .retain(.nonatomic) 466 | ) 467 | return value 468 | } 469 | } 470 | set { 471 | setAssociatedObject( 472 | self, 473 | Self.__associated_optionalIntArrayKey, 474 | newValue, 475 | .retain(.nonatomic) 476 | ) 477 | } 478 | } 479 | 480 | @inline(never) static var __associated_optionalIntArrayKey: UnsafeRawPointer { 481 | let f: @convention(c) () -> Void = { 482 | } 483 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 484 | } 485 | """ 486 | } 487 | } 488 | 489 | func testTypeDetectionOptionalDoubleArray() throws { 490 | assertMacro { 491 | """ 492 | @AssociatedObject(.retain(.nonatomic)) 493 | var optionalDoubleArray = [1.0, 2.0, 3.0, nil] 494 | """ 495 | } expansion: { 496 | """ 497 | var optionalDoubleArray { 498 | get { 499 | if let value = getAssociatedObject( 500 | self, 501 | Self.__associated_optionalDoubleArrayKey 502 | ) as? [Swift.Double?] { 503 | return value 504 | } else { 505 | let value: [Swift.Double?] = [1.0, 2.0, 3.0, nil] 506 | setAssociatedObject( 507 | self, 508 | Self.__associated_optionalDoubleArrayKey, 509 | value, 510 | .retain(.nonatomic) 511 | ) 512 | return value 513 | } 514 | } 515 | set { 516 | setAssociatedObject( 517 | self, 518 | Self.__associated_optionalDoubleArrayKey, 519 | newValue, 520 | .retain(.nonatomic) 521 | ) 522 | } 523 | } 524 | 525 | @inline(never) static var __associated_optionalDoubleArrayKey: UnsafeRawPointer { 526 | let f: @convention(c) () -> Void = { 527 | } 528 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 529 | } 530 | """ 531 | } 532 | } 533 | 534 | func testTypeDetectionOptionalIntAndDoubleArray() throws { 535 | assertMacro { 536 | """ 537 | @AssociatedObject(.retain(.nonatomic)) 538 | var doubleArray = [nil, 1, 1.0, nil, 2, 2.0, nil, 3, 3.0] 539 | """ 540 | } expansion: { 541 | """ 542 | var doubleArray { 543 | get { 544 | if let value = getAssociatedObject( 545 | self, 546 | Self.__associated_doubleArrayKey 547 | ) as? [Swift.Double?] { 548 | return value 549 | } else { 550 | let value: [Swift.Double?] = [nil, 1, 1.0, nil, 2, 2.0, nil, 3, 3.0] 551 | setAssociatedObject( 552 | self, 553 | Self.__associated_doubleArrayKey, 554 | value, 555 | .retain(.nonatomic) 556 | ) 557 | return value 558 | } 559 | } 560 | set { 561 | setAssociatedObject( 562 | self, 563 | Self.__associated_doubleArrayKey, 564 | newValue, 565 | .retain(.nonatomic) 566 | ) 567 | } 568 | } 569 | 570 | @inline(never) static var __associated_doubleArrayKey: UnsafeRawPointer { 571 | let f: @convention(c) () -> Void = { 572 | } 573 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 574 | } 575 | """ 576 | } 577 | } 578 | 579 | 580 | func testTypeDetectionOptionalBoolArray() throws { 581 | assertMacro { 582 | """ 583 | @AssociatedObject(.retain(.nonatomic)) 584 | var optionalBoolArray = [true, false, nil] 585 | """ 586 | } expansion: { 587 | """ 588 | var optionalBoolArray { 589 | get { 590 | if let value = getAssociatedObject( 591 | self, 592 | Self.__associated_optionalBoolArrayKey 593 | ) as? [Swift.Bool?] { 594 | return value 595 | } else { 596 | let value: [Swift.Bool?] = [true, false, nil] 597 | setAssociatedObject( 598 | self, 599 | Self.__associated_optionalBoolArrayKey, 600 | value, 601 | .retain(.nonatomic) 602 | ) 603 | return value 604 | } 605 | } 606 | set { 607 | setAssociatedObject( 608 | self, 609 | Self.__associated_optionalBoolArrayKey, 610 | newValue, 611 | .retain(.nonatomic) 612 | ) 613 | } 614 | } 615 | 616 | @inline(never) static var __associated_optionalBoolArrayKey: UnsafeRawPointer { 617 | let f: @convention(c) () -> Void = { 618 | } 619 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 620 | } 621 | """ 622 | } 623 | } 624 | 625 | func testTypeDetectionOptionalStringArray() throws { 626 | assertMacro { 627 | """ 628 | @AssociatedObject(.retain(.nonatomic)) 629 | var optionalStringArray = [nil, "true", "false", nil] 630 | """ 631 | } expansion: { 632 | """ 633 | var optionalStringArray { 634 | get { 635 | if let value = getAssociatedObject( 636 | self, 637 | Self.__associated_optionalStringArrayKey 638 | ) as? [Swift.String?] { 639 | return value 640 | } else { 641 | let value: [Swift.String?] = [nil, "true", "false", nil] 642 | setAssociatedObject( 643 | self, 644 | Self.__associated_optionalStringArrayKey, 645 | value, 646 | .retain(.nonatomic) 647 | ) 648 | return value 649 | } 650 | } 651 | set { 652 | setAssociatedObject( 653 | self, 654 | Self.__associated_optionalStringArrayKey, 655 | newValue, 656 | .retain(.nonatomic) 657 | ) 658 | } 659 | } 660 | 661 | @inline(never) static var __associated_optionalStringArrayKey: UnsafeRawPointer { 662 | let f: @convention(c) () -> Void = { 663 | } 664 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 665 | } 666 | """ 667 | } 668 | } 669 | } 670 | 671 | // MARK: - Dictionary 672 | extension AssociatedTypeDetectionObjectTests { 673 | func testTypeDetectionStringDictionary() throws { 674 | assertMacro { 675 | """ 676 | @AssociatedObject(.retain(.nonatomic)) 677 | var dic = ["t": "a", "s": "b"] 678 | """ 679 | } expansion: { 680 | """ 681 | var dic { 682 | get { 683 | if let value = getAssociatedObject( 684 | self, 685 | Self.__associated_dicKey 686 | ) as? [Swift.String: Swift.String] { 687 | return value 688 | } else { 689 | let value: [Swift.String: Swift.String] = ["t": "a", "s": "b"] 690 | setAssociatedObject( 691 | self, 692 | Self.__associated_dicKey, 693 | value, 694 | .retain(.nonatomic) 695 | ) 696 | return value 697 | } 698 | } 699 | set { 700 | setAssociatedObject( 701 | self, 702 | Self.__associated_dicKey, 703 | newValue, 704 | .retain(.nonatomic) 705 | ) 706 | } 707 | } 708 | 709 | @inline(never) static var __associated_dicKey: UnsafeRawPointer { 710 | let f: @convention(c) () -> Void = { 711 | } 712 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 713 | } 714 | """ 715 | } 716 | } 717 | 718 | func testTypeDetectionIntDictionary() throws { 719 | assertMacro { 720 | """ 721 | @AssociatedObject(.retain(.nonatomic)) 722 | var dic = [1: 3, 2: 4] 723 | """ 724 | } expansion: { 725 | """ 726 | var dic { 727 | get { 728 | if let value = getAssociatedObject( 729 | self, 730 | Self.__associated_dicKey 731 | ) as? [Swift.Int: Swift.Int] { 732 | return value 733 | } else { 734 | let value: [Swift.Int: Swift.Int] = [1: 3, 2: 4] 735 | setAssociatedObject( 736 | self, 737 | Self.__associated_dicKey, 738 | value, 739 | .retain(.nonatomic) 740 | ) 741 | return value 742 | } 743 | } 744 | set { 745 | setAssociatedObject( 746 | self, 747 | Self.__associated_dicKey, 748 | newValue, 749 | .retain(.nonatomic) 750 | ) 751 | } 752 | } 753 | 754 | @inline(never) static var __associated_dicKey: UnsafeRawPointer { 755 | let f: @convention(c) () -> Void = { 756 | } 757 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 758 | } 759 | """ 760 | } 761 | } 762 | 763 | func testTypeDetectionDoubleDictionary() throws { 764 | assertMacro { 765 | """ 766 | @AssociatedObject(.retain(.nonatomic)) 767 | var dic = [1.0: 3.0, 2.0: 4.0] 768 | """ 769 | } expansion: { 770 | """ 771 | var dic { 772 | get { 773 | if let value = getAssociatedObject( 774 | self, 775 | Self.__associated_dicKey 776 | ) as? [Swift.Double: Swift.Double] { 777 | return value 778 | } else { 779 | let value: [Swift.Double: Swift.Double] = [1.0: 3.0, 2.0: 4.0] 780 | setAssociatedObject( 781 | self, 782 | Self.__associated_dicKey, 783 | value, 784 | .retain(.nonatomic) 785 | ) 786 | return value 787 | } 788 | } 789 | set { 790 | setAssociatedObject( 791 | self, 792 | Self.__associated_dicKey, 793 | newValue, 794 | .retain(.nonatomic) 795 | ) 796 | } 797 | } 798 | 799 | @inline(never) static var __associated_dicKey: UnsafeRawPointer { 800 | let f: @convention(c) () -> Void = { 801 | } 802 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 803 | } 804 | """ 805 | } 806 | } 807 | } 808 | 809 | 810 | // MARK: - Other 811 | extension AssociatedTypeDetectionObjectTests { 812 | func testTypeDetection2DimensionDoubleArray() throws { 813 | assertMacro { 814 | """ 815 | @AssociatedObject(.retain(.nonatomic)) 816 | var array = [[1.0], [2.0], [3.0, 4.0]] 817 | """ 818 | } expansion: { 819 | """ 820 | var array { 821 | get { 822 | if let value = getAssociatedObject( 823 | self, 824 | Self.__associated_arrayKey 825 | ) as? [[Swift.Double]] { 826 | return value 827 | } else { 828 | let value: [[Swift.Double]] = [[1.0], [2.0], [3.0, 4.0]] 829 | setAssociatedObject( 830 | self, 831 | Self.__associated_arrayKey, 832 | value, 833 | .retain(.nonatomic) 834 | ) 835 | return value 836 | } 837 | } 838 | set { 839 | setAssociatedObject( 840 | self, 841 | Self.__associated_arrayKey, 842 | newValue, 843 | .retain(.nonatomic) 844 | ) 845 | } 846 | } 847 | 848 | @inline(never) static var __associated_arrayKey: UnsafeRawPointer { 849 | let f: @convention(c) () -> Void = { 850 | } 851 | return unsafeBitCast(f, to: UnsafeRawPointer.self) 852 | } 853 | """ 854 | } 855 | } 856 | } 857 | #endif 858 | -------------------------------------------------------------------------------- /Tests/AssociatedObjectTests/Model/ClassType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassType.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/01/19. 6 | // 7 | // 8 | 9 | import AssociatedObject 10 | 11 | class ClassType { 12 | @AssociatedObject(.retain(.nonatomic)) 13 | var int = 0 14 | 15 | @AssociatedObject(.retain(.nonatomic)) 16 | var double = 0.0 17 | 18 | @AssociatedObject(.retain(.nonatomic)) 19 | var string = "" 20 | 21 | @AssociatedObject(.retain(.nonatomic)) 22 | var bool = false 23 | 24 | @AssociatedObject(.retain(.nonatomic)) 25 | var optionalInt: Int? 26 | 27 | @AssociatedObject(.retain(.nonatomic)) 28 | var optionalDouble: Double? = 123.4 29 | 30 | @AssociatedObject(.retain(.nonatomic)) 31 | var optionalString: String? 32 | 33 | @AssociatedObject(.retain(.nonatomic)) 34 | var optionalBool: Bool? = false 35 | 36 | @AssociatedObject(.retain(.nonatomic)) 37 | var implicitlyUnwrappedString: String! 38 | 39 | @AssociatedObject(.retain(.atomic)) 40 | var intArray = [0] 41 | 42 | @AssociatedObject(.retain(.atomic)) 43 | var doubleArray = [0.0, 1] 44 | 45 | @AssociatedObject(.retain(.atomic)) 46 | var stringArray = [""] 47 | 48 | @AssociatedObject(.retain(.nonatomic)) 49 | var boolArray = [nil, false] 50 | 51 | @AssociatedObject(.retain(.atomic)) 52 | var optionalIntArray = [0, nil] 53 | 54 | @AssociatedObject(.retain(.atomic)) 55 | var optionalDoubleArray = [0.0, nil, 1] 56 | 57 | @AssociatedObject(.retain(.atomic)) 58 | var optionalStringArray = [nil, ""] 59 | 60 | @AssociatedObject(.retain(.nonatomic)) 61 | var optionalBoolArray = [false, nil] 62 | 63 | @AssociatedObject(.retain(.atomic)) 64 | var classType: ClassType2 = { 65 | .init() 66 | }() 67 | } 68 | 69 | class ClassType2 {} 70 | 71 | protocol ProtocolType: AnyObject {} 72 | 73 | extension ProtocolType { 74 | @AssociatedObject(.retain(.nonatomic)) 75 | var definedInProtocol = "hello" 76 | } 77 | 78 | extension ClassType: ProtocolType {} 79 | -------------------------------------------------------------------------------- /Tests/AssociatedObjectTests/PatternBindingSyntax+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PatternBindingSyntax+Tests.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/25. 6 | // 7 | // 8 | 9 | import XCTest 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | @testable import AssociatedObjectPlugin 13 | 14 | final class PatternBindingSyntaxTests: XCTestCase { 15 | 16 | func testSetter() { 17 | let setter = AccessorDeclSyntax(accessorSpecifier: .keyword(.set), body: .init(statements: CodeBlockItemListSyntax {})) 18 | 19 | let binding: PatternBindingSyntax = .init( 20 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 21 | accessorBlock: .init( 22 | accessors: .accessors(.init { 23 | setter 24 | }) 25 | ) 26 | ) 27 | 28 | XCTAssertEqual(setter.description, binding.setter?.description) 29 | } 30 | 31 | func testGetter() { 32 | let getter = AccessorDeclSyntax(accessorSpecifier: .keyword(.get), body: .init(statements: CodeBlockItemListSyntax {})) 33 | 34 | var binding: PatternBindingSyntax = .init( 35 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 36 | accessorBlock: .init( 37 | accessors: .accessors(.init { 38 | getter 39 | }) 40 | ) 41 | ) 42 | 43 | XCTAssertEqual(getter.description, binding.getter?.description) 44 | 45 | /* getter only */ 46 | guard let body = getter.body else { 47 | XCTFail("body must not be nil") 48 | return 49 | } 50 | binding = .init( 51 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 52 | accessorBlock: .init( 53 | accessors: .getter(body.statements) 54 | ) 55 | ) 56 | 57 | XCTAssertEqual(getter.description, binding.getter?.description) 58 | } 59 | 60 | func testSetSetter() { 61 | let setter = AccessorDeclSyntax(accessorSpecifier: .keyword(.set), body: .init(statements: CodeBlockItemListSyntax {})) 62 | var binding: PatternBindingSyntax = .init( 63 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 64 | accessorBlock: .init( 65 | accessors: .accessors(.init { 66 | setter 67 | }) 68 | ) 69 | ) 70 | let newSetter = AccessorDeclSyntax( 71 | accessorSpecifier: .keyword(.set), 72 | body: .init(statements: CodeBlockItemListSyntax { 73 | .init(item: .expr("print(\"hello\")")) 74 | }) 75 | ) 76 | 77 | binding.setter = newSetter 78 | XCTAssertEqual(newSetter.description, binding.setter?.description) 79 | 80 | 81 | /* getter only */ 82 | binding = .init( 83 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 84 | accessorBlock: .init( 85 | accessors: .getter(.init { 86 | DeclSyntax("\"hello\"") 87 | }) 88 | ) 89 | ) 90 | 91 | binding.setter = newSetter 92 | XCTAssertEqual(newSetter.description, binding.setter?.description) 93 | } 94 | 95 | func testSetGetter() { 96 | let getter = AccessorDeclSyntax(accessorSpecifier: .keyword(.get), body: .init(statements: CodeBlockItemListSyntax {})) 97 | var binding: PatternBindingSyntax = .init( 98 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 99 | accessorBlock: .init( 100 | accessors: .accessors(.init { 101 | getter 102 | }) 103 | ) 104 | ) 105 | 106 | let newGetter = AccessorDeclSyntax( 107 | accessorSpecifier: .keyword(.get), 108 | body: .init(statements: CodeBlockItemListSyntax { 109 | .init(item: .decl("\"hello\"")) 110 | }) 111 | ) 112 | 113 | binding.getter = newGetter 114 | XCTAssertEqual(newGetter.description, binding.getter?.description) 115 | 116 | /* getter only */ 117 | binding = .init( 118 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 119 | accessorBlock: .init( 120 | accessors: .getter(.init { 121 | DeclSyntax("\"hello\"") 122 | }) 123 | ) 124 | ) 125 | 126 | binding.getter = newGetter 127 | XCTAssertEqual(newGetter.description, binding.getter?.description) 128 | 129 | /* setter only */ 130 | binding = .init( 131 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 132 | accessorBlock: .init( 133 | accessors: .accessors(.init { 134 | AccessorDeclSyntax(accessorSpecifier: .keyword(.set), body: .init(statements: CodeBlockItemListSyntax {})) 135 | }) 136 | ) 137 | ) 138 | 139 | binding.getter = newGetter 140 | XCTAssertEqual(newGetter.description, binding.getter?.description) 141 | } 142 | 143 | func testWillSet() { 144 | let `willSet` = AccessorDeclSyntax(accessorSpecifier: .keyword(.willSet), body: .init(statements: CodeBlockItemListSyntax {})) 145 | 146 | let binding: PatternBindingSyntax = .init( 147 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 148 | accessorBlock: .init( 149 | accessors: .accessors(.init { 150 | `willSet` 151 | }) 152 | ) 153 | ) 154 | 155 | XCTAssertEqual(`willSet`.description, binding.willSet?.description) 156 | } 157 | 158 | func testDidSet() { 159 | let `didSet` = AccessorDeclSyntax(accessorSpecifier: .keyword(.didSet), body: .init(statements: CodeBlockItemListSyntax {})) 160 | 161 | let binding: PatternBindingSyntax = .init( 162 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 163 | accessorBlock: .init( 164 | accessors: .accessors(.init { 165 | `didSet` 166 | }) 167 | ) 168 | ) 169 | 170 | XCTAssertEqual(`didSet`.description, binding.didSet?.description) 171 | } 172 | 173 | func testSetWillSet() { 174 | let `willSet` = AccessorDeclSyntax(accessorSpecifier: .keyword(.willSet), body: .init(statements: CodeBlockItemListSyntax {})) 175 | 176 | var binding: PatternBindingSyntax = .init( 177 | pattern: IdentifierPatternSyntax(identifier: .identifier("value")), 178 | accessorBlock: .init( 179 | accessors: .accessors(.init { 180 | `willSet` 181 | }) 182 | ) 183 | ) 184 | 185 | let newWillSet = AccessorDeclSyntax( 186 | accessorSpecifier: .keyword(.willSet), 187 | body: .init(statements: CodeBlockItemListSyntax { 188 | .init(item: .decl("\"hello\"")) 189 | }) 190 | ) 191 | 192 | binding.willSet = newWillSet 193 | XCTAssertEqual(newWillSet.description, binding.willSet?.description) 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/AssociatedObject/ba5859d881ac1b814e77e2f1b39235dfe71a8d1f/image.png --------------------------------------------------------------------------------