Skip to content

Commit 2af6ac2

Browse files
authored
Gracefully handle recursive state debug output (#117)
* Gracefully handle recursive state debug output * Fix
1 parent 6f3749b commit 2af6ac2

File tree

2 files changed

+153
-94
lines changed

2 files changed

+153
-94
lines changed

Sources/ComposableArchitecture/Internal/Debug.swift

Lines changed: 137 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,147 @@
11
import Foundation
22

33
func debugOutput(_ value: Any, indent: Int = 0) -> String {
4-
let mirror = Mirror(reflecting: value)
5-
switch (value, mirror.displayStyle) {
6-
case let (value as CustomDebugOutputConvertible, _):
7-
return value.debugOutput.indent(by: indent)
8-
case (_, .collection?):
9-
return """
10-
[
11-
\(mirror.children.map { "\(debugOutput($0.value, indent: 2)),\n" }.joined())]
12-
"""
13-
.indent(by: indent)
14-
case (_, .dictionary?):
15-
let pairs = mirror.children.map { label, value -> String in
16-
let pair = value as! (key: AnyHashable, value: Any)
17-
return "\("\(debugOutput(pair.key.base)): \(debugOutput(pair.value)),".indent(by: 2))\n"
18-
}
19-
return """
20-
[
21-
\(pairs.sorted().joined())]
22-
"""
23-
.indent(by: indent)
24-
case (_, .set?):
25-
return """
26-
Set([
27-
\(mirror.children.map { "\(debugOutput($0.value, indent: 2)),\n" }.sorted().joined())])
28-
"""
29-
.indent(by: indent)
30-
case (_, .optional?):
31-
return mirror.children.isEmpty
32-
? "nil".indent(by: indent)
33-
: debugOutput(mirror.children.first!.value, indent: indent)
34-
case (_, .enum?) where !mirror.children.isEmpty:
35-
let child = mirror.children.first!
36-
let childMirror = Mirror(reflecting: child.value)
37-
let elements =
38-
childMirror.displayStyle != .tuple
39-
? debugOutput(child.value, indent: 2)
40-
: childMirror.children.map { child -> String in
4+
var visitedItems: Set<ObjectIdentifier> = []
5+
6+
func debugOutputHelp(_ value: Any, indent: Int = 0) -> String {
7+
let mirror = Mirror(reflecting: value)
8+
switch (value, mirror.displayStyle) {
9+
case let (value as CustomDebugOutputConvertible, _):
10+
return value.debugOutput.indent(by: indent)
11+
case (_, .collection?):
12+
return """
13+
[
14+
\(mirror.children.map { "\(debugOutput($0.value, indent: 2)),\n" }.joined())]
15+
"""
16+
.indent(by: indent)
17+
18+
case (_, .dictionary?):
19+
let pairs = mirror.children.map { label, value -> String in
20+
let pair = value as! (key: AnyHashable, value: Any)
21+
return
22+
"\("\(debugOutputHelp(pair.key.base)): \(debugOutputHelp(pair.value)),".indent(by: 2))\n"
23+
}
24+
return """
25+
[
26+
\(pairs.sorted().joined())]
27+
"""
28+
.indent(by: indent)
29+
30+
case (_, .set?):
31+
return """
32+
Set([
33+
\(mirror.children.map { "\(debugOutputHelp($0.value, indent: 2)),\n" }.sorted().joined())])
34+
"""
35+
.indent(by: indent)
36+
37+
case (_, .optional?):
38+
return mirror.children.isEmpty
39+
? "nil".indent(by: indent)
40+
: debugOutputHelp(mirror.children.first!.value, indent: indent)
41+
42+
case (_, .enum?) where !mirror.children.isEmpty:
43+
let child = mirror.children.first!
44+
let childMirror = Mirror(reflecting: child.value)
45+
let elements =
46+
childMirror.displayStyle != .tuple
47+
? debugOutputHelp(child.value, indent: 2)
48+
: childMirror.children.map { child -> String in
49+
let label = child.label!
50+
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutputHelp(child.value))"
51+
}
52+
.joined(separator: ",\n")
53+
.indent(by: 2)
54+
return """
55+
\(mirror.subjectType).\(child.label!)(
56+
\(elements)
57+
)
58+
"""
59+
.indent(by: indent)
60+
61+
case (_, .enum?):
62+
return """
63+
\(mirror.subjectType).\(value)
64+
"""
65+
.indent(by: indent)
66+
67+
case (_, .struct?) where !mirror.children.isEmpty:
68+
let elements = mirror.children
69+
.map { "\($0.label.map { "\($0): " } ?? "")\(debugOutputHelp($0.value))".indent(by: 2) }
70+
.joined(separator: ",\n")
71+
return """
72+
\(mirror.subjectType)(
73+
\(elements)
74+
)
75+
"""
76+
.indent(by: indent)
77+
78+
case let (value as AnyObject, .class?)
79+
where !mirror.children.isEmpty && !visitedItems.contains(ObjectIdentifier(value)):
80+
visitedItems.insert(ObjectIdentifier(value))
81+
let elements = mirror.children
82+
.map { "\($0.label.map { "\($0): " } ?? "")\(debugOutputHelp($0.value))".indent(by: 2) }
83+
.joined(separator: ",\n")
84+
return """
85+
\(mirror.subjectType)(
86+
\(elements)
87+
)
88+
"""
89+
.indent(by: indent)
90+
91+
case let (value as AnyObject, .class?)
92+
where !mirror.children.isEmpty && visitedItems.contains(ObjectIdentifier(value)):
93+
return "\(mirror.subjectType)(↩︎)"
94+
95+
case let (value as CustomStringConvertible, .class?):
96+
return value.description
97+
.replacingOccurrences(
98+
of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression
99+
)
100+
.indent(by: indent)
101+
102+
case let (value as CustomDebugStringConvertible, _):
103+
return value.debugDescription
104+
.replacingOccurrences(
105+
of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression
106+
)
107+
.indent(by: indent)
108+
109+
case let (value as CustomStringConvertible, _):
110+
return value.description
111+
.indent(by: indent)
112+
113+
case (_, .struct?), (_, .class?):
114+
return "\(mirror.subjectType)()"
115+
.indent(by: indent)
116+
117+
case (_, .tuple?) where mirror.children.isEmpty:
118+
return "()"
119+
.indent(by: indent)
120+
121+
case (_, .tuple?):
122+
let elements = mirror.children.map { child -> String in
41123
let label = child.label!
42-
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutput(child.value))"
124+
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutputHelp(child.value))"
125+
.indent(by: 2)
43126
}
44-
.joined(separator: ",\n")
45-
.indent(by: 2)
46-
return """
47-
\(mirror.subjectType).\(child.label!)(
48-
\(elements)
49-
)
50-
"""
51-
.indent(by: indent)
52-
case (_, .enum?):
53-
return """
54-
\(mirror.subjectType).\(value)
55-
"""
56-
.indent(by: indent)
57-
case (_, .struct?) where !mirror.children.isEmpty, (_, .class?) where !mirror.children.isEmpty:
58-
let elements = mirror.children
59-
.map { "\($0.label.map { "\($0): " } ?? "")\(debugOutput($0.value))".indent(by: 2) }
60-
.joined(separator: ",\n")
61-
return """
62-
\(mirror.subjectType)(
63-
\(elements)
64-
)
65-
"""
66-
.indent(by: indent)
67-
case let (value as CustomStringConvertible, .class?):
68-
return value.description
69-
.replacingOccurrences(of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression)
70-
.indent(by: indent)
71-
case let (value as CustomDebugStringConvertible, _):
72-
return value.debugDescription
73-
.replacingOccurrences(of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression)
74-
.indent(by: indent)
75-
case let (value as CustomStringConvertible, _):
76-
return value.description
77-
.indent(by: indent)
78-
case (_, .struct?), (_, .class?):
79-
return "\(mirror.subjectType)()"
80-
.indent(by: indent)
81-
case (_, .tuple?) where mirror.children.isEmpty:
82-
return "()"
83-
.indent(by: indent)
84-
case (_, .tuple?):
85-
let elements = mirror.children.map { child -> String in
86-
let label = child.label!
87-
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutput(child.value))".indent(by: 2)
127+
return """
128+
(
129+
\(elements.joined(separator: ",\n"))
130+
)
131+
"""
132+
.indent(by: indent)
133+
134+
case (_, nil):
135+
return "\(value)"
136+
.indent(by: indent)
137+
138+
@unknown default:
139+
return "\(value)"
140+
.indent(by: indent)
88141
}
89-
return """
90-
(
91-
\(elements.joined(separator: ",\n"))
92-
)
93-
"""
94-
.indent(by: indent)
95-
case (_, nil):
96-
return "\(value)"
97-
.indent(by: indent)
98-
@unknown default:
99-
return "\(value)"
100-
.indent(by: indent)
101142
}
143+
144+
return debugOutputHelp(value, indent: indent)
102145
}
103146

104147
func debugDiff<T>(_ before: T, _ after: T, printer: (T) -> String = { debugOutput($0) }) -> String?

Tests/ComposableArchitectureTests/DebugTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,22 @@ final class DebugTests: XCTestCase {
373373
)
374374
}
375375

376+
func testRecursiveOutput() {
377+
class Foo {
378+
var foo: Foo?
379+
}
380+
let foo = Foo()
381+
foo.foo = foo
382+
XCTAssertEqual(
383+
debugOutput(foo),
384+
"""
385+
Foo(
386+
foo: Foo(↩︎)
387+
)
388+
"""
389+
)
390+
}
391+
376392
func testEffectOutput() {
377393
// XCTAssertEqual(
378394
// Effect<Int, Never>(value: 42)

0 commit comments

Comments
 (0)