Skip to content

Commit

Permalink
DependencyClient/Endpoint accessor fixes (#163)
Browse files Browse the repository at this point in the history
* Fixes

* more
  • Loading branch information
stephencelis committed Dec 15, 2023
1 parent 101ba87 commit 940f266
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 12 deletions.
16 changes: 13 additions & 3 deletions Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
) throws -> [AttributeSyntax] {
guard
let property = member.as(VariableDeclSyntax.self),
property.bindingSpecifier.tokenKind != .keyword(.let),
property.isClosure,
let binding = property.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed,
Expand Down Expand Up @@ -85,7 +86,8 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
var accesses: Set<Access> = Access(modifiers: declaration.modifiers).map { [$0] } ?? []
for member in declaration.memberBlock.members {
guard var property = member.decl.as(VariableDeclSyntax.self) else { continue }
let isEndpoint = property.hasDependencyEndpointMacroAttached || property.isClosure
let isEndpoint = property.hasDependencyEndpointMacroAttached
|| property.bindingSpecifier.tokenKind != .keyword(.let) && property.isClosure
let propertyAccess = Access(modifiers: property.modifiers)
guard
var binding = property.bindings.first,
Expand All @@ -95,8 +97,15 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
if property.bindingSpecifier.tokenKind == .keyword(.let), binding.initializer != nil {
continue
}
if let accessors = binding.accessorBlock?.accessors, case .getter = accessors {
continue
if let accessors = binding.accessorBlock?.accessors {
switch accessors {
case .getter:
continue
case let .accessors(accessors):
if accessors.contains(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) {
continue
}
}
}

if propertyAccess == .private, binding.initializer != nil { continue }
Expand Down Expand Up @@ -162,6 +171,7 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
)
}
if isEndpoint {
binding.accessorBlock = nil
binding.initializer = nil
} else if binding.initializer == nil, type.is(OptionalTypeSyntax.self) {
binding.typeAnnotation?.trailingTrivia = .space
Expand Down
104 changes: 97 additions & 7 deletions Sources/DependenciesMacrosPlugin/DependencyEndpointMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
let property = declaration.as(VariableDeclSyntax.self),
let binding = property.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed,
let type = binding.typeAnnotation?.type.trimmed,
let functionType = property.asClosureType?.trimmed
else {
context.diagnose(
Expand All @@ -71,8 +70,8 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
)
return []
}
let unescapedIdentifier = identifier.trimmedDescription.trimmedBackticks

let unescapedIdentifier = identifier.trimmedDescription.trimmedBackticks
var unimplementedDefault: ClosureExprSyntax
if let initializer = binding.initializer {
guard var closure = initializer.value.as(ClosureExprSyntax.self)
Expand Down Expand Up @@ -189,11 +188,102 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
)
}

return decls + [
"""
private var _\(raw: unescapedIdentifier): \(raw: type) = \(unimplementedDefault)
"""
]
let privateProperty = property.privatePrefixed("_", unimplementedDefault: unimplementedDefault)

return decls + [privateProperty.cast(DeclSyntax.self)]
}
}

extension TokenSyntax {
func privatePrefixed(_ prefix: String) -> TokenSyntax {
switch tokenKind {
case .identifier(let identifier):
return TokenSyntax(
.identifier(prefix + identifier.trimmedBackticks), leadingTrivia: leadingTrivia,
trailingTrivia: trailingTrivia, presence: presence)
default:
return self
}
}
}

extension PatternBindingListSyntax {
func privatePrefixed(
_ prefix: String, unimplementedDefault: ClosureExprSyntax
) -> PatternBindingListSyntax {
var bindings = self.map { $0 }
for index in 0..<bindings.count {
let binding = bindings[index]
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {
bindings[index] = PatternBindingSyntax(
leadingTrivia: binding.leadingTrivia,
pattern: IdentifierPatternSyntax(
leadingTrivia: identifier.leadingTrivia,
identifier: identifier.identifier.privatePrefixed(prefix),
trailingTrivia: identifier.trailingTrivia
),
typeAnnotation: binding.typeAnnotation,
initializer: InitializerClauseSyntax(value: unimplementedDefault),
accessorBlock: binding.accessorBlock,
trailingComma: binding.trailingComma,
trailingTrivia: binding.trailingTrivia
)
}
}

return PatternBindingListSyntax(bindings)
}
}

extension DeclModifierListSyntax {
func privatePrefixed(_ prefix: String) -> DeclModifierListSyntax {
let modifier: DeclModifierSyntax = DeclModifierSyntax(name: "private")
return [modifier]
+ filter {
switch $0.name.tokenKind {
case .keyword(let keyword):
switch keyword {
case .fileprivate, .private, .internal, .public:
return false
default:
return true
}
default:
return true
}
}
}

init(keyword: Keyword) {
self.init([DeclModifierSyntax(name: .keyword(keyword))])
}
}

extension VariableDeclSyntax {
func privatePrefixed(
_ prefix: String, unimplementedDefault: ClosureExprSyntax
) -> VariableDeclSyntax {
var attributes = self.attributes
for index in attributes.indices.reversed() {
if case let .attribute(attribute) = attributes[index],
attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "DependencyEndpoint"
{
attributes.remove(at: index)
}
}
return VariableDeclSyntax(
leadingTrivia: leadingTrivia,
attributes: attributes,
modifiers: modifiers.privatePrefixed(prefix),
bindingSpecifier: TokenSyntax(
bindingSpecifier.tokenKind,
leadingTrivia: .space,
trailingTrivia: .space,
presence: .present
),
bindings: bindings.privatePrefixed(prefix, unimplementedDefault: unimplementedDefault),
trailingTrivia: trailingTrivia
)
}
}

Expand Down
112 changes: 110 additions & 2 deletions Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@ final class DependencyClientMacroTests: BaseTestCase {
}
}

func testLetBinding() {
assertMacro {
"""
@DependencyClient
struct Client {
var endpoint: () -> Void
let config: () -> Void
}
"""
} expansion: {
"""
struct Client {
@DependencyEndpoint
var endpoint: () -> Void
let config: () -> Void
init(
endpoint: @escaping () -> Void,
config: @escaping () -> Void
) {
self.endpoint = endpoint
self.config = config
}
init(
config: @escaping () -> Void
) {
self.config = config
}
}
"""
}
}

func testBooleanLiteral() {
assertMacro {
"""
Expand Down Expand Up @@ -452,6 +486,80 @@ final class DependencyClientMacroTests: BaseTestCase {
}
}

func testComputedPropertyGet() {
assertMacro {
"""
@DependencyClient
struct Client: Sendable {
var endpoint: @Sendable () -> Void
var name: String {
get {
"Blob"
}
}
}
"""
} expansion: {
"""
struct Client: Sendable {
@DependencyEndpoint
var endpoint: @Sendable () -> Void
var name: String {
get {
"Blob"
}
}
init(
endpoint: @Sendable @escaping () -> Void
) {
self.endpoint = endpoint
}
init() {
}
}
"""
}
}

func testComputedPropertyWillSet() {
assertMacro {
"""
@DependencyClient
struct Client: Sendable {
var endpoint: @Sendable () throws -> Void {
willSet {
print("!")
}
}
}
"""
} expansion: {
"""
struct Client: Sendable {
@DependencyEndpoint
var endpoint: @Sendable () throws -> Void {
willSet {
print("!")
}
}
init(
endpoint: @Sendable @escaping () throws -> Void
) {
self.endpoint = endpoint
}
init() {
}
}
"""
}
}

func testLet_WithDefault() {
assertMacro {
"""
Expand Down Expand Up @@ -587,7 +695,7 @@ final class DependencyClientMacroTests: BaseTestCase {
try self.fetch(p0)
}
private var _fetch: (_ id: Int) throws -> String = { _ in
@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
XCTestDynamicOverlay.XCTFail("Unimplemented: 'fetch'")
throw DependenciesMacros.Unimplemented("fetch")
}
Expand Down Expand Up @@ -635,7 +743,7 @@ final class DependencyClientMacroTests: BaseTestCase {
try self.fetch(p0)
}
private var _fetch: (_ id: Int) throws -> String = { _ in
@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
XCTestDynamicOverlay.XCTFail("Unimplemented: 'fetch'")
throw DependenciesMacros.Unimplemented("fetch")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -944,4 +944,50 @@ final class DependencyEndpointMacroTests: BaseTestCase {
"""
}
}

func testWillSet() {
assertMacro {
"""
struct Blah {
@DependencyEndpoint
public var foo: () throws -> Void {
willSet {
print("!")
}
}
}
"""
} expansion: {
"""
struct Blah {
public var foo: () throws -> Void {
willSet {
print("!")
}
@storageRestrictions(initializes: _foo)
init(initialValue) {
_foo = initialValue
}
get {
_foo
}
set {
_foo = newValue
}
}
private var _foo: () throws -> Void = {
XCTestDynamicOverlay.XCTFail("Unimplemented: 'foo'")
throw DependenciesMacros.Unimplemented("foo")
} {
willSet {
print("!")
}
}
}
"""
}
}
}

0 comments on commit 940f266

Please sign in to comment.