Skip to content

Commit 63301f4

Browse files
authored
Fix a few macro issues (#137)
- Introduce better diagnostics when defaulting with a non-closure literal, like `unimplemented` - Support multiline default closures.
1 parent 1b0c4b5 commit 63301f4

File tree

5 files changed

+134
-10
lines changed

5 files changed

+134
-10
lines changed

Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
2121
else {
2222
return []
2323
}
24-
// NB: Ideally `@DependencyEndpoint` would handle this for us, but there's a compiler crash.
25-
if binding.initializer == nil,
26-
functionType.effectSpecifiers?.throwsSpecifier == nil,
24+
// NB: Ideally `@DependencyEndpoint` would handle this for us, but there are compiler crashes
25+
if let initializer = binding.initializer {
26+
try initializer.diagnose(node)
27+
} else if functionType.effectSpecifiers?.throwsSpecifier == nil,
2728
!functionType.isVoid,
2829
!functionType.isOptional
2930
{
@@ -156,12 +157,13 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
156157
binding.pattern.trailingTrivia = ""
157158
binding.typeAnnotation = TypeAnnotationSyntax(
158159
colon: .colonToken(trailingTrivia: .space),
159-
type: type
160+
type: type.with(\.trailingTrivia, .space)
160161
)
161162
}
162163
if isEndpoint {
163164
binding.initializer = nil
164165
} else if binding.initializer == nil, type.is(OptionalTypeSyntax.self) {
166+
binding.typeAnnotation?.trailingTrivia = .space
165167
binding.initializer = InitializerClauseSyntax(
166168
equal: .equalToken(trailingTrivia: .space),
167169
value: NilLiteralExprSyntax()

Sources/DependenciesMacrosPlugin/DependencyEndpointMacro.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
2121
else {
2222
return []
2323
}
24-
24+
if let initializer = binding.initializer {
25+
try initializer.diagnose(node)
26+
}
2527
return [
2628
"""
2729
@storageRestrictions(initializes: _\(raw: identifier))
@@ -72,7 +74,6 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
7274
if let initializer = binding.initializer {
7375
guard var closure = initializer.value.as(ClosureExprSyntax.self)
7476
else {
75-
// TODO: Diagnose?
7677
return []
7778
}
7879
if !functionType.isVoid,
@@ -83,7 +84,7 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
8384
statement.item = CodeBlockItemSyntax.Item(
8485
ReturnStmtSyntax(
8586
returnKeyword: .keyword(.return, trailingTrivia: .space),
86-
expression: expression
87+
expression: expression.trimmed
8788
)
8889
)
8990
closure.statements = closure.statements.with(\.[closure.statements.startIndex], statement)
@@ -121,7 +122,11 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
121122
""",
122123
at: unimplementedDefault.statements.startIndex
123124
)
124-
125+
for index in unimplementedDefault.statements.indices {
126+
unimplementedDefault.statements[index] = unimplementedDefault.statements[index]
127+
.trimmed
128+
.with(\.leadingTrivia, .newline)
129+
}
125130
var effectSpecifiers = ""
126131
if functionType.effectSpecifiers?.throwsSpecifier != nil {
127132
effectSpecifiers.append("try ")

Sources/DependenciesMacrosPlugin/Support.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,39 @@ extension FunctionTypeSyntax {
7070
}
7171
}
7272

73+
extension InitializerClauseSyntax {
74+
func diagnose(_ attribute: AttributeSyntax) throws {
75+
guard !self.value.is(ClosureExprSyntax.self) else { return }
76+
var diagnostics: [Diagnostic] = [
77+
Diagnostic(
78+
node: self.value,
79+
message: MacroExpansionErrorMessage(
80+
"""
81+
'@\(attribute.attributeName)' default must be closure literal
82+
"""
83+
)
84+
)
85+
]
86+
if self.value.as(FunctionCallExprSyntax.self)?
87+
.calledExpression.as(DeclReferenceExprSyntax.self)?
88+
.baseName.tokenKind == .identifier("unimplemented")
89+
{
90+
diagnostics.append(
91+
Diagnostic(
92+
node: self.value,
93+
message: MacroExpansionWarningMessage(
94+
"""
95+
Do not use 'unimplemented' with '@\(attribute.attributeName)'; it is a replacement and \
96+
implements the same runtime functionality as 'unimplemented' at compile time
97+
"""
98+
)
99+
)
100+
)
101+
}
102+
throw DiagnosticsError(diagnostics: diagnostics)
103+
}
104+
}
105+
73106
extension VariableDeclSyntax {
74107
var asClosureType: FunctionTypeSyntax? {
75108
self.bindings.first?.typeAnnotation.flatMap {

Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ final class DependencyClientMacroTests: BaseTestCase {
506506
✏️ Insert '= { <#Int#> }'
507507
}
508508
"""
509-
}fixes: {
509+
} fixes: {
510510
"""
511511
@DependencyClient
512512
struct Client: Sendable {
@@ -720,4 +720,26 @@ final class DependencyClientMacroTests: BaseTestCase {
720720
"""
721721
}
722722
}
723+
724+
func testNonClosureDefault() {
725+
assertMacro {
726+
"""
727+
@DependencyClient
728+
struct Foo {
729+
var bar: () -> Int = unimplemented()
730+
}
731+
"""
732+
} diagnostics: {
733+
"""
734+
@DependencyClient
735+
struct Foo {
736+
var bar: () -> Int = unimplemented()
737+
┬──────────────
738+
├─ 🛑 '@DependencyClient' default must be closure literal
739+
╰─ ⚠️ Do not use 'unimplemented' with '@DependencyClient'; it is a replacement and implements the same runtime functionality as 'unimplemented' at compile time
740+
}
741+
"""
742+
}
743+
}
723744
}
745+

Tests/DependenciesMacrosPluginTests/DependencyEndpointMacroTests.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ final class DependencyEndpointMacroTests: BaseTestCase {
9595
✏️ Insert '= { <#Bool#> }'
9696
}
9797
"""
98-
}fixes: {
98+
} fixes: {
9999
"""
100100
struct Client {
101101
@DependencyEndpoint
@@ -693,4 +693,66 @@ final class DependencyEndpointMacroTests: BaseTestCase {
693693
"""
694694
}
695695
}
696+
697+
func testNonClosureDefault() {
698+
assertMacro {
699+
"""
700+
struct Foo {
701+
@DependencyEndpoint
702+
var bar: () -> Int = unimplemented()
703+
}
704+
"""
705+
} diagnostics: {
706+
"""
707+
struct Foo {
708+
@DependencyEndpoint
709+
var bar: () -> Int = unimplemented()
710+
┬──────────────
711+
├─ 🛑 '@DependencyEndpoint' default must be closure literal
712+
╰─ ⚠️ Do not use 'unimplemented' with '@DependencyEndpoint'; it is a replacement and implements the same runtime functionality as 'unimplemented' at compile time
713+
}
714+
"""
715+
}
716+
}
717+
718+
func testMultilineClosure() {
719+
assertMacro {
720+
"""
721+
struct Blah {
722+
@DependencyEndpoint
723+
public var doAThing: (_ value: Int) -> String = { _ in
724+
"Hello, world"
725+
}
726+
}
727+
"""
728+
} expansion: {
729+
"""
730+
struct Blah {
731+
public var doAThing: (_ value: Int) -> String = { _ in
732+
"Hello, world"
733+
} {
734+
@storageRestrictions(initializes: _doAThing)
735+
init(initialValue) {
736+
_doAThing = initialValue
737+
}
738+
get {
739+
_doAThing
740+
}
741+
set {
742+
_doAThing = newValue
743+
}
744+
}
745+
746+
public func doAThing(value p0: Int) -> String {
747+
self.doAThing(p0)
748+
}
749+
750+
private var _doAThing: (_ value: Int) -> String = { _ in
751+
XCTestDynamicOverlay.XCTFail("Unimplemented: 'doAThing'")
752+
return "Hello, world"
753+
}
754+
}
755+
"""
756+
}
757+
}
696758
}

0 commit comments

Comments
 (0)