├── .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 | [](https://github.com/p-x9/AssociatedObject/issues)
8 | [](https://github.com/p-x9/AssociatedObject/network/members)
9 | [](https://github.com/p-x9/AssociatedObject/stargazers)
10 | [](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 | 
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
--------------------------------------------------------------------------------