Skip to content

Commit bb10116

Browse files
authored
Merge pull request #7 from p-x9/feature/support-wiiset-and-didset
support `wiiSet` and `didSet`
2 parents 5057cc7 + db15991 commit bb10116

File tree

4 files changed

+269
-29
lines changed

4 files changed

+269
-29
lines changed

README.md

+43
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,49 @@ class ViewController: UIViewController {
3434

3535
}
3636
```
37+
### willSet/didSet
38+
Properties defined using `@AssociatedObject` can implement willSet and didSet.
39+
In swift, it is not possible to implement `willSet` and `didSet` at the same time as setter, so they are expanded as follows.
40+
41+
```swift
42+
@AssociatedObject(.OBJC_ASSOCIATION_COPY_NONATOMIC)
43+
public var hello: String = "こんにちは" {
44+
didSet {
45+
print("didSet")
46+
}
47+
willSet {
48+
print("willSet: \(newValue)")
49+
}
50+
}
51+
52+
// ↓↓↓ expand to ... ↓↓↓
53+
public var hello: String = "こんにちは" {
54+
get {
55+
objc_getAssociatedObject(
56+
self,
57+
&Self.__associated_helloKey
58+
) as? String
59+
?? "こんにちは"
60+
}
61+
62+
set {
63+
let willSet: (String) -> Void = { newValue in
64+
print("willSet: \(newValue)")
65+
}
66+
willSet(newValue)
67+
objc_setAssociatedObject(
68+
self,
69+
&Self.__associated_helloKey,
70+
newValue,
71+
.OBJC_ASSOCIATION_COPY_NONATOMIC
72+
)
73+
let didSet = {
74+
print("didSet")
75+
}
76+
didSet()
77+
}
78+
}
79+
```
3780

3881
## License
3982
AssociatedObject is released under the MIT License. See [LICENSE](./LICENSE)

Sources/AssociatedObjectPlugin/AssociatedObjectMacro.swift

+56-23
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,15 @@ extension AssociatedObjectMacro: AccessorMacro {
6565
return []
6666
}
6767

68-
// Error if accessor already exists
69-
if let accessor = binding.accessor {
70-
context.diagnose(AssociatedObjectMacroDiagnostic.accessorShouldBeNil.diagnose(at: accessor))
68+
// Error if setter already exists
69+
if let setter = binding.setter {
70+
context.diagnose(AssociatedObjectMacroDiagnostic.getterAndSetterShouldBeNil.diagnose(at: setter))
71+
return []
72+
}
73+
74+
// Error if getter already exists
75+
if let getter = binding.getter {
76+
context.diagnose(AssociatedObjectMacroDiagnostic.getterAndSetterShouldBeNil.diagnose(at: getter))
7177
return []
7278
}
7379

@@ -83,28 +89,55 @@ extension AssociatedObjectMacro: AccessorMacro {
8389
return []
8490
}
8591

86-
return [
87-
"""
88-
get {
89-
objc_getAssociatedObject(
90-
self,
91-
&Self.__associated_\(identifier)Key
92-
) as? \(type)
93-
?? \(defaultValue)
94-
}
95-
""",
92+
if let willSet = binding.willSet,
93+
let parameter = willSet.parameter,
94+
parameter.name.trimmed.text != "newValue" {
95+
context.diagnose(AssociatedObjectMacroDiagnostic.accessorParameterNameMustBeNewValue.diagnose(at: parameter))
96+
return []
97+
}
9698

97-
"""
99+
return [
100+
AccessorDeclSyntax(
101+
accessorKind: .keyword(.get),
102+
body: CodeBlockSyntax {
103+
"""
104+
objc_getAssociatedObject(
105+
self,
106+
&Self.__associated_\(identifier)Key
107+
) as? \(type)
108+
?? \(defaultValue)
109+
"""
110+
}
111+
),
98112

99-
set {
100-
objc_setAssociatedObject(
101-
self,
102-
&Self.__associated_\(identifier)Key,
103-
newValue,
104-
\(policy)
105-
)
106-
}
107-
"""
113+
AccessorDeclSyntax(
114+
accessorKind: .keyword(.set),
115+
body: CodeBlockSyntax {
116+
if let willSet = binding.willSet?.body {
117+
"""
118+
let willSet: (\(type.trimmed)) -> Void = { newValue in
119+
\(willSet.statements.trimmed)
120+
}
121+
willSet(newValue)
122+
"""
123+
}
124+
"""
125+
objc_setAssociatedObject(
126+
self,
127+
&Self.__associated_\(identifier)Key,
128+
newValue,
129+
\(policy)
130+
)
131+
"""
132+
if let didSet = binding.didSet?.body {
133+
"""
134+
let didSet = {
135+
\(didSet.statements.trimmed)
136+
}
137+
didSet()
138+
"""
139+
}
140+
})
108141
]
109142
}
110143
}

Sources/AssociatedObjectPlugin/AssociatedObjectMacroDiagnostic.swift

+9-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import SwiftDiagnostics
1111

1212
public enum AssociatedObjectMacroDiagnostic {
1313
case requiresVariableDeclaration
14-
case accessorShouldBeNil
14+
case getterAndSetterShouldBeNil
15+
case accessorParameterNameMustBeNewValue
1516
case requiresInitialValue
1617
case specifyTypeExplicitly
1718
}
@@ -24,13 +25,15 @@ extension AssociatedObjectMacroDiagnostic: DiagnosticMessage {
2425
public var message: String {
2526
switch self {
2627
case .requiresVariableDeclaration:
27-
return "`@AssociatedObject` must be appended to the property declaration."
28-
case .accessorShouldBeNil:
29-
return "`accessor should not be specified."
28+
return "`@AssociatedObject` must be attached to the property declaration."
29+
case .getterAndSetterShouldBeNil:
30+
return "getter and setter must not be implemented when using `@AssociatedObject`."
31+
case .accessorParameterNameMustBeNewValue:
32+
return "accessor parameter name must be `newValue` when using `@AssociatedObject`."
3033
case .requiresInitialValue:
31-
return "Initial values must be specified."
34+
return "Initial values must be specified when using `@AssociatedObject`."
3235
case .specifyTypeExplicitly:
33-
return "Specify a type explicitly"
36+
return "Specify a type explicitly when using `@AssociatedObject`."
3437
}
3538
}
3639

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
if case let .accessors(list) = accessor {
16+
return list.accessors.first(where: {
17+
$0.accessorKind.tokenKind == .keyword(.set)
18+
})
19+
}
20+
return nil
21+
}
22+
23+
set {
24+
// NOTE: Be careful that setter cannot be implemented without a getter.
25+
setNewAccessor(kind: .keyword(.set), newValue: newValue)
26+
}
27+
}
28+
29+
var getter: AccessorDeclSyntax? {
30+
get {
31+
switch accessor {
32+
case let .accessors(list):
33+
return list.accessors.first(where: {
34+
$0.accessorKind.tokenKind == .keyword(.get)
35+
})
36+
case let .getter(body):
37+
return AccessorDeclSyntax(accessorKind: .keyword(.get), body: body)
38+
case .none:
39+
return nil
40+
}
41+
}
42+
43+
set {
44+
switch accessor {
45+
case .getter, .none:
46+
if let newValue {
47+
if let body = newValue.body {
48+
accessor = .getter(body)
49+
} else {
50+
let accessors = AccessorListSyntax {
51+
newValue
52+
}
53+
accessor = .accessors(.init(accessors: accessors))
54+
}
55+
} else {
56+
accessor = .none
57+
}
58+
59+
case let .accessors(block):
60+
var accessors = block.accessors
61+
for (i, accessor) in block.accessors.enumerated() {
62+
guard accessor.accessorKind.tokenKind == .keyword(.get) else {
63+
continue
64+
}
65+
if let newValue {
66+
accessors = accessors.replacing(childAt: i, with: newValue)
67+
} else {
68+
accessors = accessors.removing(childAt: i)
69+
}
70+
break
71+
}
72+
let newBlock = block.with(\.accessors, accessors)
73+
accessor = .accessors(newBlock)
74+
}
75+
}
76+
}
77+
78+
var isGetOnly: Bool {
79+
if initializer != nil {
80+
return false
81+
}
82+
if case let .accessors(list) = accessor,
83+
list.accessors.contains(where: { $0.accessorKind.tokenKind == .keyword(.set) }) {
84+
return false
85+
}
86+
if accessor == nil && initializer == nil {
87+
return false
88+
}
89+
return true
90+
}
91+
}
92+
93+
extension PatternBindingSyntax {
94+
var willSet: AccessorDeclSyntax? {
95+
get {
96+
if case let .accessors(list) = accessor {
97+
return list.accessors.first(where: {
98+
$0.accessorKind.tokenKind == .keyword(.willSet)
99+
})
100+
}
101+
return nil
102+
}
103+
set {
104+
// NOTE: Be careful that willSet cannot be implemented without a setter.
105+
setNewAccessor(kind: .keyword(.willSet), newValue: newValue)
106+
}
107+
}
108+
109+
var didSet: AccessorDeclSyntax? {
110+
get {
111+
if case let .accessors(list) = accessor {
112+
return list.accessors.first(where: {
113+
$0.accessorKind.tokenKind == .keyword(.didSet)
114+
})
115+
}
116+
return nil
117+
}
118+
set {
119+
// NOTE: Be careful that willSet cannot be implemented without a setter.
120+
setNewAccessor(kind: .keyword(.willSet), newValue: newValue)
121+
}
122+
}
123+
}
124+
125+
extension PatternBindingSyntax {
126+
// NOTE: - getter requires extra steps and should not be used.
127+
private mutating func setNewAccessor(kind: TokenKind, newValue: AccessorDeclSyntax?) {
128+
var newAccessor: Accessor?
129+
130+
switch accessor {
131+
case let .getter(body):
132+
guard let newValue else { return }
133+
newAccessor = .accessors(.init(accessors: AccessorListSyntax {
134+
AccessorDeclSyntax(accessorKind: .keyword(.get), body: body)
135+
newValue
136+
}))
137+
case let .accessors(block):
138+
var accessors = block.accessors
139+
for (i, accessor) in block.accessors.enumerated() {
140+
guard accessor.accessorKind.tokenKind == kind else {
141+
continue
142+
}
143+
if let newValue {
144+
accessors = accessors.replacing(childAt: i, with: newValue)
145+
} else {
146+
accessors = accessors.removing(childAt: i)
147+
}
148+
break
149+
}
150+
let newBlock = block.with(\.accessors, accessors)
151+
newAccessor = .accessors(newBlock)
152+
case .none:
153+
guard let newValue else { return }
154+
newAccessor = .accessors(.init(accessors: AccessorListSyntax {
155+
newValue
156+
}))
157+
}
158+
159+
self.accessor = newAccessor
160+
}
161+
}

0 commit comments

Comments
 (0)