Skip to content

Commit 9af8876

Browse files
Fix fatalerror default (#158)
* Allow for fatalError in default values. * wip * wip * Update Sources/DependenciesMacros/Macros.swift * wip * clean up * wip * wip * wip * wip * wip * wip * wip * fix --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 6c2bbde commit 9af8876

File tree

5 files changed

+251
-48
lines changed

5 files changed

+251
-48
lines changed

Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro {
2222
return []
2323
}
2424
// NB: Ideally `@DependencyEndpoint` would handle this for us, but there are compiler crashes
25-
if let initializer = binding.initializer {
26-
try initializer.diagnose(node)
25+
if
26+
let initializer = binding.initializer,
27+
try initializer.diagnose(node, context: context).earlyOut
28+
{
29+
return []
2730
} else if functionType.effectSpecifiers?.throwsSpecifier == nil,
2831
!functionType.isVoid,
2932
!functionType.isOptional

Sources/DependenciesMacrosPlugin/DependencyEndpointMacro.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
2121
else {
2222
return []
2323
}
24-
if let initializer = binding.initializer {
25-
try initializer.diagnose(node)
24+
if let initializer = binding.initializer,
25+
try initializer.diagnose(node, context: context).earlyOut
26+
{
27+
return []
2628
}
29+
2730
return [
2831
"""
2932
@storageRestrictions(initializes: _\(raw: identifier))
@@ -76,17 +79,21 @@ public enum DependencyEndpointMacro: AccessorMacro, PeerMacro {
7679
else {
7780
return []
7881
}
79-
if !functionType.isVoid,
80-
closure.statements.count == 1,
82+
if closure.statements.count == 1,
8183
var statement = closure.statements.first,
82-
let expression = statement.item.as(ExprSyntax.self)
84+
let expression = statement.item.as(ExprSyntax.self),
85+
!functionType.isVoid
86+
|| expression.as(FunctionCallExprSyntax.self)?.calledExpression.is(ClosureExprSyntax.self)
87+
== true
8388
{
84-
statement.item = CodeBlockItemSyntax.Item(
85-
ReturnStmtSyntax(
86-
returnKeyword: .keyword(.return, trailingTrivia: .space),
87-
expression: expression.trimmed
89+
if !statement.item.description.hasPrefix("fatalError(") {
90+
statement.item = CodeBlockItemSyntax.Item(
91+
ReturnStmtSyntax(
92+
returnKeyword: .keyword(.return, trailingTrivia: .space),
93+
expression: expression.trimmed
94+
)
8895
)
89-
)
96+
}
9097
closure.statements = closure.statements.with(\.[closure.statements.startIndex], statement)
9198
}
9299
unimplementedDefault = closure

Sources/DependenciesMacrosPlugin/Support.swift

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,38 +71,98 @@ extension FunctionTypeSyntax {
7171
}
7272

7373
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(
74+
func diagnose(
75+
_ attribute: AttributeSyntax,
76+
context: some MacroExpansionContext
77+
) throws -> DiagnosticAction {
78+
guard let closure = self.value.as(ClosureExprSyntax.self)
79+
else {
80+
var diagnostics: [Diagnostic] = [
81+
Diagnostic(
82+
node: self.value,
83+
message: MacroExpansionErrorMessage(
8084
"""
8185
'@\(attribute.attributeName)' default must be closure literal
8286
"""
87+
)
8388
)
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(
89+
]
90+
if self.value.as(FunctionCallExprSyntax.self)?
91+
.calledExpression.as(DeclReferenceExprSyntax.self)?
92+
.baseName.tokenKind == .identifier("unimplemented")
93+
{
94+
diagnostics.append(
95+
Diagnostic(
96+
node: self.value,
97+
message: MacroExpansionWarningMessage(
9498
"""
9599
Do not use 'unimplemented' with '@\(attribute.attributeName)'; it is a replacement and \
96100
implements the same runtime functionality as 'unimplemented' at compile time
97101
"""
102+
)
98103
)
99104
)
100-
)
105+
}
106+
throw DiagnosticsError(diagnostics: diagnostics)
101107
}
102-
throw DiagnosticsError(diagnostics: diagnostics)
108+
109+
guard
110+
closure.statements.count == 1,
111+
let statement = closure.statements.first,
112+
statement.item.description.hasPrefix("fatalError(")
113+
else {
114+
return DiagnosticAction(earlyOut: false)
115+
}
116+
117+
context.diagnose(
118+
Diagnostic(
119+
node: statement.item,
120+
message: MacroExpansionWarningMessage(
121+
"""
122+
Prefer returning a default mock value over 'fatalError()' to avoid crashes in previews \
123+
and tests.
124+
125+
The default value can be anything and does not need to signify a real value. For \
126+
example, if the endpoint returns a boolean, you can return 'false', or if it returns an \
127+
array, you can return '[]'.
128+
"""
129+
),
130+
fixIt: FixIt(
131+
message: MacroExpansionFixItMessage(
132+
"""
133+
Wrap in a synchronously executed closure to silence this warning
134+
"""
135+
),
136+
changes: [
137+
.replace(
138+
oldNode: Syntax(statement),
139+
newNode: Syntax(
140+
FunctionCallExprSyntax(
141+
calledExpression: ClosureExprSyntax(
142+
statements: [
143+
statement.with(\.leadingTrivia, .space)
144+
]
145+
),
146+
leftParen: .leftParenToken(),
147+
arguments: [],
148+
rightParen: .rightParenToken()
149+
)
150+
.with(\.trailingTrivia, .space)
151+
)
152+
)
153+
]
154+
)
155+
)
156+
)
157+
158+
return DiagnosticAction(earlyOut: true)
103159
}
104160
}
105161

162+
struct DiagnosticAction {
163+
let earlyOut: Bool
164+
}
165+
106166
extension VariableDeclSyntax {
107167
var asClosureType: FunctionTypeSyntax? {
108168
self.bindings.first?.typeAnnotation.flatMap {

Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -513,22 +513,6 @@ final class DependencyClientMacroTests: BaseTestCase {
513513
var endpoint: @Sendable () -> Int = { <#Int#> }
514514
}
515515
"""
516-
} expansion: {
517-
"""
518-
struct Client: Sendable {
519-
@DependencyEndpoint
520-
var endpoint: @Sendable () -> Int = { <#Int#> }
521-
522-
init(
523-
endpoint: @Sendable @escaping () -> Int
524-
) {
525-
self.endpoint = endpoint
526-
}
527-
528-
init() {
529-
}
530-
}
531-
"""
532516
}
533517
}
534518

@@ -741,5 +725,42 @@ final class DependencyClientMacroTests: BaseTestCase {
741725
"""
742726
}
743727
}
744-
}
745728

729+
func testFatalError() {
730+
assertMacro {
731+
"""
732+
@DependencyClient
733+
struct Blah {
734+
public var foo: () -> String = { fatalError() }
735+
public var bar: () -> String = { fatalError("Goodbye") }
736+
}
737+
"""
738+
} diagnostics: {
739+
"""
740+
@DependencyClient
741+
struct Blah {
742+
public var foo: () -> String = { fatalError() }
743+
┬───────────
744+
╰─ ⚠️ Prefer returning a default mock value over 'fatalError()' to avoid crashes in previews and tests.
745+
746+
The default value can be anything and does not need to signify a real value. For example, if the endpoint returns a boolean, you can return 'false', or if it returns an array, you can return '[]'.
747+
✏️ Wrap in a synchronously executed closure to silence this warning
748+
public var bar: () -> String = { fatalError("Goodbye") }
749+
┬────────────────────
750+
╰─ ⚠️ Prefer returning a default mock value over 'fatalError()' to avoid crashes in previews and tests.
751+
752+
The default value can be anything and does not need to signify a real value. For example, if the endpoint returns a boolean, you can return 'false', or if it returns an array, you can return '[]'.
753+
✏️ Wrap in a synchronously executed closure to silence this warning
754+
}
755+
"""
756+
} fixes: {
757+
"""
758+
@DependencyClient
759+
struct Blah {
760+
public var foo: () -> String = { { fatalError() }() }
761+
public var bar: () -> String = { { fatalError("Goodbye") }() }
762+
}
763+
"""
764+
}
765+
}
766+
}

Tests/DependenciesMacrosPluginTests/DependencyEndpointMacroTests.swift

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ final class DependencyEndpointMacroTests: BaseTestCase {
145145
✏️ Insert '= { _, _, _ in <#Bool#> }'
146146
}
147147
"""
148-
}fixes: {
148+
} fixes: {
149149
"""
150150
struct Client {
151151
@DependencyEndpoint
@@ -832,4 +832,116 @@ final class DependencyEndpointMacroTests: BaseTestCase {
832832
"""
833833
}
834834
}
835+
836+
func testFatalError() {
837+
assertMacro {
838+
"""
839+
struct Blah {
840+
@DependencyEndpoint
841+
public var foo: () -> String = { fatalError() }
842+
@DependencyEndpoint
843+
public var bar: () -> String = { fatalError("Goodbye") }
844+
}
845+
"""
846+
} diagnostics: {
847+
"""
848+
struct Blah {
849+
@DependencyEndpoint
850+
public var foo: () -> String = { fatalError() }
851+
┬ ────────────
852+
├─ ⚠️ Prefer returning a default mock value over 'fatalError()' to avoid crashes in previews and tests.
853+
854+
The default value can be anything and does not need to signify a real value. For example, if the endpoint returns a boolean, you can return 'false', or if it returns an array, you can return '[]'.
855+
│ ✏️ Wrap in a synchronously executed closure to silence this warning │ ╰─ ⚠️ Prefer returning a default mock value over 'fatalError()' to avoid crashes in previews and tests.
856+
857+
The default value can be anything and does not need to signify a real value. For example, if the endpoint returns a boolean, you can return 'false', or if it returns an array, you can return '[]'.
858+
✏️ Wrap in a synchronously executed closure to silence this warning
859+
@DependencyEndpoint
860+
public var bar: () -> String = { fatalError("Goodbye") }
861+
}
862+
"""
863+
} fixes: {
864+
"""
865+
struct Blah {
866+
@DependencyEndpoint
867+
public var foo: () -> String = { fatalError() }
868+
@DependencyEndpoint
869+
public var bar: () -> String = { fatalError("Goodbye") }
870+
}
871+
"""
872+
} expansion: {
873+
"""
874+
struct Blah {
875+
public var foo: () -> String = { fatalError() }
876+
877+
private var _foo: () -> String = {
878+
XCTestDynamicOverlay.XCTFail("Unimplemented: 'foo'")
879+
fatalError()
880+
}
881+
public var bar: () -> String = { fatalError("Goodbye") }
882+
883+
private var _bar: () -> String = {
884+
XCTestDynamicOverlay.XCTFail("Unimplemented: 'bar'")
885+
fatalError("Goodbye")
886+
}
887+
}
888+
"""
889+
}
890+
}
891+
892+
func testFatalError_SilenceWarning() {
893+
assertMacro {
894+
"""
895+
struct Blah {
896+
@DependencyEndpoint
897+
public var foo: () -> Void = { { fatalError() }() }
898+
@DependencyEndpoint
899+
public var bar: () -> String = { { fatalError("Goodbye") }() }
900+
}
901+
"""
902+
} expansion: {
903+
"""
904+
struct Blah {
905+
public var foo: () -> Void = { { fatalError() }() } {
906+
@storageRestrictions(initializes: _foo)
907+
init(initialValue) {
908+
_foo = initialValue
909+
}
910+
get {
911+
_foo
912+
}
913+
set {
914+
_foo = newValue
915+
}
916+
}
917+
918+
private var _foo: () -> Void = {
919+
XCTestDynamicOverlay.XCTFail("Unimplemented: 'foo'")
920+
return {
921+
fatalError()
922+
}()
923+
}
924+
public var bar: () -> String = { { fatalError("Goodbye") }() } {
925+
@storageRestrictions(initializes: _bar)
926+
init(initialValue) {
927+
_bar = initialValue
928+
}
929+
get {
930+
_bar
931+
}
932+
set {
933+
_bar = newValue
934+
}
935+
}
936+
937+
private var _bar: () -> String = {
938+
XCTestDynamicOverlay.XCTFail("Unimplemented: 'bar'")
939+
return {
940+
fatalError("Goodbye")
941+
}()
942+
}
943+
}
944+
"""
945+
}
946+
}
835947
}

0 commit comments

Comments
 (0)