Skip to content

Commit d4d9cc2

Browse files
authored
[Debug] Escape LLDB syntax in debugDescription (#75300)
- **Explanation**: When using `@DebugDescription`, only allow use of [LLDB Summary Strings](https://lldb.llvm.org/use/variable.html#summary-strings) syntax from `lldbDescription` properties. When `@DebugDescription` is applied to existing `debugDescription` properties, escape any `$`, as the output of `debugDescription` is never interpreted by LLDB. - **Scope**: This will not break existing code. Adopters will almost certainly be unaffected. - **Original PRs**: #75300 - **Risk**: No risk, it fixes a potentially surprising bug if an existing `debugDescription` was reused with this macro, and the string contained the substring `${`. - **Testing**: Swift tests, CI. - **Reviewers**: @hborla, @stephentyrone, @DougGregor
1 parent 8d0d9d0 commit d4d9cc2

File tree

5 files changed

+68
-7
lines changed

5 files changed

+68
-7
lines changed

lib/Macros/Sources/SwiftMacros/DebugDescriptionMacro.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import SwiftSyntax
1414
import SwiftSyntaxMacros
1515
import SwiftDiagnostics
16+
import _StringProcessing
1617

1718
public enum DebugDescriptionMacro {}
1819
public enum _DebugDescriptionPropertyMacro {}
@@ -188,12 +189,20 @@ extension _DebugDescriptionPropertyMacro: PeerMacro {
188189
return []
189190
}
190191

192+
// LLDB syntax is not allowed in debugDescription/description.
193+
let allowLLDBSyntax = onlyBinding.name == "lldbDescription"
194+
191195
// Iterate the string's segments, and convert property expressions into LLDB variable references.
192196
var summarySegments: [String] = []
193197
for segment in descriptionString.segments {
194198
switch segment {
195199
case let .stringSegment(segment):
196-
summarySegments.append(segment.content.text)
200+
var literal = segment.content.text
201+
if !allowLLDBSyntax {
202+
// To match debugDescription/description, escape `$` characters. LLDB must treat them as a literals they are.
203+
literal = literal.escapedForLLDB()
204+
}
205+
summarySegments.append(literal)
197206
case let .expressionSegment(segment):
198207
guard let onlyLabeledExpr = segment.expressions.only, onlyLabeledExpr.label == nil else {
199208
// This catches `appendInterpolation` overrides.
@@ -262,7 +271,7 @@ extension _DebugDescriptionPropertyMacro: PeerMacro {
262271

263272
/// The names of properties that can be converted to LLDB type summaries, in priority order.
264273
fileprivate let DESCRIPTION_PROPERTIES = [
265-
"_debugDescription",
274+
"lldbDescription",
266275
"debugDescription",
267276
"description",
268277
]
@@ -503,6 +512,28 @@ extension String {
503512
}
504513
}
505514

515+
extension String {
516+
fileprivate func escapedForLLDB() -> String {
517+
guard #available(macOS 13, *) else {
518+
guard self.firstIndex(of: "$") != nil else {
519+
return self
520+
}
521+
522+
var result = ""
523+
for char in self {
524+
if char == "$" {
525+
result.append("\\$")
526+
} else {
527+
result.append(char)
528+
}
529+
}
530+
return result
531+
}
532+
533+
return self.replacing("$", with: "\\$")
534+
}
535+
}
536+
506537
extension Array where Element == String {
507538
/// Convert an ArrayExprSyntax consisting of StringLiteralExprSyntax to an Array<String>.
508539
fileprivate init?(expr: ExprSyntax) {

stdlib/public/core/DebuggerSupport.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ import SwiftShims
4444
/// }
4545
///
4646
/// The `DebugDescription` macro supports both `debugDescription`, `description`,
47-
/// as well as a third option: a property named `_debugDescription`. The first
47+
/// as well as a third option: a property named `lldbDescription`. The first
4848
/// two are implemented when conforming to the `CustomDebugStringConvertible`
49-
/// and `CustomStringConvertible` protocols. The additional `_debugDescription`
49+
/// and `CustomStringConvertible` protocols. The additional `lldbDescription`
5050
/// property is useful when both `debugDescription` and `description` are
5151
/// implemented, but don't meet the requirements of the `DebugDescription`
52-
/// macro. If `_debugDescription` is implemented, `DebugDescription` choose it
52+
/// macro. If `lldbDescription` is implemented, `DebugDescription` choose it
5353
/// over `debugDescription` and `description`. Likewise, `debugDescription` is
5454
/// preferred over `description`.
5555
///

stdlib/public/core/ObjectIdentifier+DebugDescription.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#if !$Embedded
1414
@DebugDescription
1515
extension ObjectIdentifier {
16-
var _debugDescription: String {
16+
var lldbDescription: String {
1717
return "ObjectIdentifier(\(_value))"
1818
}
1919
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// REQUIRES: swift_swift_parser
2+
3+
// RUN: %empty-directory(%t)
4+
// RUN: %target-swift-frontend %s -swift-version 5 -module-name main -disable-availability-checking -typecheck -enable-experimental-feature DebugDescriptionMacro -plugin-path %swift-plugin-dir -dump-macro-expansions > %t/expansions-dump.txt 2>&1
5+
// RUN: %FileCheck %s < %t/expansions-dump.txt
6+
7+
@DebugDescription
8+
struct MyStruct {
9+
var name: String = "thirty"
10+
var debugDescription: String { "${\(self.name)}" }
11+
}
12+
// CHECK: static let _lldb_summary = (
13+
// CHECK: /* version */ 1 as UInt8,
14+
// CHECK: /* record size */ 32 as UInt8,
15+
// CHECK: /* "main.MyStruct" */ 14 as UInt8, 109 as UInt8, 97 as UInt8, 105 as UInt8, 110 as UInt8, 46 as UInt8, 77 as UInt8, 121 as UInt8, 83 as UInt8, 116 as UInt8, 114 as UInt8, 117 as UInt8, 99 as UInt8, 116 as UInt8, 0 as UInt8,
16+
// CHECK: /* "\${${var.name}}" */ 16 as UInt8, 92 as UInt8, 36 as UInt8, 123 as UInt8, 36 as UInt8, 123 as UInt8, 118 as UInt8, 97 as UInt8, 114 as UInt8, 46 as UInt8, 110 as UInt8, 97 as UInt8, 109 as UInt8, 101 as UInt8, 125 as UInt8, 125 as UInt8, 0 as UInt8
17+
// CHECK: )
18+
19+
@DebugDescription
20+
class MyClass {
21+
var name: String = "thirty"
22+
var lldbDescription: String { "${var.name}" }
23+
}
24+
// CHECK: static let _lldb_summary = (
25+
// CHECK: /* version */ 1 as UInt8,
26+
// CHECK: /* record size */ 27 as UInt8,
27+
// CHECK: /* "main.MyClass" */ 13 as UInt8, 109 as UInt8, 97 as UInt8, 105 as UInt8, 110 as UInt8, 46 as UInt8, 77 as UInt8, 121 as UInt8, 67 as UInt8, 108 as UInt8, 97 as UInt8, 115 as UInt8, 115 as UInt8, 0 as UInt8,
28+
// CHECK: /* "${var.name}" */ 12 as UInt8, 36 as UInt8, 123 as UInt8, 118 as UInt8, 97 as UInt8, 114 as UInt8, 46 as UInt8, 110 as UInt8, 97 as UInt8, 109 as UInt8, 101 as UInt8, 125 as UInt8, 0 as UInt8
29+
// CHECK: )
30+

test/Macros/DebugDescription/supported_description.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct MyStruct2: CustomDebugStringConvertible {
3131
struct MyStruct3: CustomDebugStringConvertible {
3232
var description: String { "thirty" }
3333
var debugDescription: String { "eleven" }
34-
var _debugDescription: String { "two" }
34+
var lldbDescription: String { "two" }
3535
}
3636
// CHECK: static let _lldb_summary = (
3737
// CHECK: /* version */ 1 as UInt8,

0 commit comments

Comments
 (0)