Skip to content

Commit 940f266

Browse files
authored
DependencyClient/Endpoint accessor fixes (#163)
* Fixes * more
1 parent 101ba87 commit 940f266

File tree

4 files changed

+266
-12
lines changed

4 files changed

+266
-12
lines changed

Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
1414
) throws -> [AttributeSyntax] {
1515
guard
1616
let property = member.as(VariableDeclSyntax.self),
17+
property.bindingSpecifier.tokenKind != .keyword(.let),
1718
property.isClosure,
1819
let binding = property.bindings.first,
1920
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed,
@@ -85,7 +86,8 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
8586
var accesses: Set<Access> = Access(modifiers: declaration.modifiers).map { [$0] } ?? []
8687
for member in declaration.memberBlock.members {
8788
guard var property = member.decl.as(VariableDeclSyntax.self) else { continue }
88-
let isEndpoint = property.hasDependencyEndpointMacroAttached || property.isClosure
89+
let isEndpoint = property.hasDependencyEndpointMacroAttached
90+
|| property.bindingSpecifier.tokenKind != .keyword(.let) && property.isClosure
8991
let propertyAccess = Access(modifiers: property.modifiers)
9092
guard
9193
var binding = property.bindings.first,
@@ -95,8 +97,15 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
9597
if property.bindingSpecifier.tokenKind == .keyword(.let), binding.initializer != nil {
9698
continue
9799
}
98-
if let accessors = binding.accessorBlock?.accessors, case .getter = accessors {
99-
continue
100+
if let accessors = binding.accessorBlock?.accessors {
101+
switch accessors {
102+
case .getter:
103+
continue
104+
case let .accessors(accessors):
105+
if accessors.contains(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) {
106+
continue
107+
}
108+
}
100109
}
101110

102111
if propertyAccess == .private, binding.initializer != nil { continue }
@@ -162,6 +171,7 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
162171
)
163172
}
164173
if isEndpoint {
174+
binding.accessorBlock = nil
165175
binding.initializer = nil
166176
} else if binding.initializer == nil, type.is(OptionalTypeSyntax.self) {
167177
binding.typeAnnotation?.trailingTrivia = .space

Sources/DependenciesMacrosPlugin/DependencyEndpointMacro.swift

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
5656
let property = declaration.as(VariableDeclSyntax.self),
5757
let binding = property.bindings.first,
5858
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed,
59-
let type = binding.typeAnnotation?.type.trimmed,
6059
let functionType = property.asClosureType?.trimmed
6160
else {
6261
context.diagnose(
@@ -71,8 +70,8 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
7170
)
7271
return []
7372
}
74-
let unescapedIdentifier = identifier.trimmedDescription.trimmedBackticks
7573

74+
let unescapedIdentifier = identifier.trimmedDescription.trimmedBackticks
7675
var unimplementedDefault: ClosureExprSyntax
7776
if let initializer = binding.initializer {
7877
guard var closure = initializer.value.as(ClosureExprSyntax.self)
@@ -189,11 +188,102 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
189188
)
190189
}
191190

192-
return decls + [
193-
"""
194-
private var _\(raw: unescapedIdentifier): \(raw: type) = \(unimplementedDefault)
195-
"""
196-
]
191+
let privateProperty = property.privatePrefixed("_", unimplementedDefault: unimplementedDefault)
192+
193+
return decls + [privateProperty.cast(DeclSyntax.self)]
194+
}
195+
}
196+
197+
extension TokenSyntax {
198+
func privatePrefixed(_ prefix: String) -> TokenSyntax {
199+
switch tokenKind {
200+
case .identifier(let identifier):
201+
return TokenSyntax(
202+
.identifier(prefix + identifier.trimmedBackticks), leadingTrivia: leadingTrivia,
203+
trailingTrivia: trailingTrivia, presence: presence)
204+
default:
205+
return self
206+
}
207+
}
208+
}
209+
210+
extension PatternBindingListSyntax {
211+
func privatePrefixed(
212+
_ prefix: String, unimplementedDefault: ClosureExprSyntax
213+
) -> PatternBindingListSyntax {
214+
var bindings = self.map { $0 }
215+
for index in 0..<bindings.count {
216+
let binding = bindings[index]
217+
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {
218+
bindings[index] = PatternBindingSyntax(
219+
leadingTrivia: binding.leadingTrivia,
220+
pattern: IdentifierPatternSyntax(
221+
leadingTrivia: identifier.leadingTrivia,
222+
identifier: identifier.identifier.privatePrefixed(prefix),
223+
trailingTrivia: identifier.trailingTrivia
224+
),
225+
typeAnnotation: binding.typeAnnotation,
226+
initializer: InitializerClauseSyntax(value: unimplementedDefault),
227+
accessorBlock: binding.accessorBlock,
228+
trailingComma: binding.trailingComma,
229+
trailingTrivia: binding.trailingTrivia
230+
)
231+
}
232+
}
233+
234+
return PatternBindingListSyntax(bindings)
235+
}
236+
}
237+
238+
extension DeclModifierListSyntax {
239+
func privatePrefixed(_ prefix: String) -> DeclModifierListSyntax {
240+
let modifier: DeclModifierSyntax = DeclModifierSyntax(name: "private")
241+
return [modifier]
242+
+ filter {
243+
switch $0.name.tokenKind {
244+
case .keyword(let keyword):
245+
switch keyword {
246+
case .fileprivate, .private, .internal, .public:
247+
return false
248+
default:
249+
return true
250+
}
251+
default:
252+
return true
253+
}
254+
}
255+
}
256+
257+
init(keyword: Keyword) {
258+
self.init([DeclModifierSyntax(name: .keyword(keyword))])
259+
}
260+
}
261+
262+
extension VariableDeclSyntax {
263+
func privatePrefixed(
264+
_ prefix: String, unimplementedDefault: ClosureExprSyntax
265+
) -> VariableDeclSyntax {
266+
var attributes = self.attributes
267+
for index in attributes.indices.reversed() {
268+
if case let .attribute(attribute) = attributes[index],
269+
attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "DependencyEndpoint"
270+
{
271+
attributes.remove(at: index)
272+
}
273+
}
274+
return VariableDeclSyntax(
275+
leadingTrivia: leadingTrivia,
276+
attributes: attributes,
277+
modifiers: modifiers.privatePrefixed(prefix),
278+
bindingSpecifier: TokenSyntax(
279+
bindingSpecifier.tokenKind,
280+
leadingTrivia: .space,
281+
trailingTrivia: .space,
282+
presence: .present
283+
),
284+
bindings: bindings.privatePrefixed(prefix, unimplementedDefault: unimplementedDefault),
285+
trailingTrivia: trailingTrivia
286+
)
197287
}
198288
}
199289

Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,40 @@ final class DependencyClientMacroTests: BaseTestCase {
7272
}
7373
}
7474

75+
func testLetBinding() {
76+
assertMacro {
77+
"""
78+
@DependencyClient
79+
struct Client {
80+
var endpoint: () -> Void
81+
let config: () -> Void
82+
}
83+
"""
84+
} expansion: {
85+
"""
86+
struct Client {
87+
@DependencyEndpoint
88+
var endpoint: () -> Void
89+
let config: () -> Void
90+
91+
init(
92+
endpoint: @escaping () -> Void,
93+
config: @escaping () -> Void
94+
) {
95+
self.endpoint = endpoint
96+
self.config = config
97+
}
98+
99+
init(
100+
config: @escaping () -> Void
101+
) {
102+
self.config = config
103+
}
104+
}
105+
"""
106+
}
107+
}
108+
75109
func testBooleanLiteral() {
76110
assertMacro {
77111
"""
@@ -452,6 +486,80 @@ final class DependencyClientMacroTests: BaseTestCase {
452486
}
453487
}
454488

489+
func testComputedPropertyGet() {
490+
assertMacro {
491+
"""
492+
@DependencyClient
493+
struct Client: Sendable {
494+
var endpoint: @Sendable () -> Void
495+
496+
var name: String {
497+
get {
498+
"Blob"
499+
}
500+
}
501+
}
502+
"""
503+
} expansion: {
504+
"""
505+
struct Client: Sendable {
506+
@DependencyEndpoint
507+
var endpoint: @Sendable () -> Void
508+
509+
var name: String {
510+
get {
511+
"Blob"
512+
}
513+
}
514+
515+
init(
516+
endpoint: @Sendable @escaping () -> Void
517+
) {
518+
self.endpoint = endpoint
519+
}
520+
521+
init() {
522+
}
523+
}
524+
"""
525+
}
526+
}
527+
528+
func testComputedPropertyWillSet() {
529+
assertMacro {
530+
"""
531+
@DependencyClient
532+
struct Client: Sendable {
533+
var endpoint: @Sendable () throws -> Void {
534+
willSet {
535+
print("!")
536+
}
537+
}
538+
}
539+
"""
540+
} expansion: {
541+
"""
542+
struct Client: Sendable {
543+
@DependencyEndpoint
544+
var endpoint: @Sendable () throws -> Void {
545+
willSet {
546+
print("!")
547+
}
548+
}
549+
550+
init(
551+
endpoint: @Sendable @escaping () throws -> Void
552+
) {
553+
self.endpoint = endpoint
554+
}
555+
556+
init() {
557+
}
558+
}
559+
"""
560+
}
561+
}
562+
455563
func testLet_WithDefault() {
456564
assertMacro {
457565
"""
@@ -587,7 +695,7 @@ final class DependencyClientMacroTests: BaseTestCase {
587695
try self.fetch(p0)
588696
}
589697
590-
private var _fetch: (_ id: Int) throws -> String = { _ in
698+
@available(iOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") @available(macOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") @available(tvOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") @available(watchOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") private var _fetch: (_ id: Int) throws -> String = { _ in
591699
XCTestDynamicOverlay.XCTFail("Unimplemented: 'fetch'")
592700
throw DependenciesMacros.Unimplemented("fetch")
593701
}
@@ -635,7 +743,7 @@ final class DependencyClientMacroTests: BaseTestCase {
635743
try self.fetch(p0)
636744
}
637745
638-
private var _fetch: (_ id: Int) throws -> String = { _ in
746+
@available(iOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") @available(macOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") @available(tvOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") @available(watchOS, deprecated: 9999, message: "This property has a method equivalent that is preferred for autocomplete via this deprecation. It is perfectly fine to use for overriding and accessing via '@Dependency'.") private var _fetch: (_ id: Int) throws -> String = { _ in
639747
XCTestDynamicOverlay.XCTFail("Unimplemented: 'fetch'")
640748
throw DependenciesMacros.Unimplemented("fetch")
641749
}

Tests/DependenciesMacrosPluginTests/DependencyEndpointMacroTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,4 +944,50 @@ final class DependencyEndpointMacroTests: BaseTestCase {
944944
"""
945945
}
946946
}
947+
948+
func testWillSet() {
949+
assertMacro {
950+
"""
951+
struct Blah {
952+
@DependencyEndpoint
953+
public var foo: () throws -> Void {
954+
willSet {
955+
print("!")
956+
}
957+
}
958+
}
959+
"""
960+
} expansion: {
961+
"""
962+
struct Blah {
963+
public var foo: () throws -> Void {
964+
willSet {
965+
print("!")
966+
}
967+
@storageRestrictions(initializes: _foo)
968+
init(initialValue) {
969+
_foo = initialValue
970+
}
971+
972+
get {
973+
_foo
974+
}
975+
976+
set {
977+
_foo = newValue
978+
}
979+
}
980+
981+
private var _foo: () throws -> Void = {
982+
XCTestDynamicOverlay.XCTFail("Unimplemented: 'foo'")
983+
throw DependenciesMacros.Unimplemented("foo")
984+
} {
985+
willSet {
986+
print("!")
987+
}
988+
}
989+
}
990+
"""
991+
}
992+
}
947993
}

0 commit comments

Comments
 (0)