Skip to content

Commit 4551d21

Browse files
authored
Merge pull request #63048 from lorentey/fix-String-debugDescription
[stdlib] String.debugDescription: Fix quoting behavior
2 parents a5bcf13 + 1241df3 commit 4551d21

File tree

3 files changed

+93
-7
lines changed

3 files changed

+93
-7
lines changed

stdlib/public/core/String.swift

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,11 +676,49 @@ extension String: ExpressibleByStringLiteral {
676676
extension String: CustomDebugStringConvertible {
677677
/// A representation of the string that is suitable for debugging.
678678
public var debugDescription: String {
679+
func hasBreak(between left: String, and right: Unicode.Scalar) -> Bool {
680+
// Note: we know `left` ends with an ASCII character, so we only need to
681+
// look at its last scalar.
682+
var state = _GraphemeBreakingState()
683+
return state.shouldBreak(between: left.unicodeScalars.last!, and: right)
684+
}
685+
686+
// Prevent unquoted scalars in the string from combining with the opening
687+
// `"` or the tail of the preceding quoted scalar.
679688
var result = "\""
689+
var wantBreak = true // true if next scalar must not combine with the last
680690
for us in self.unicodeScalars {
681-
result += us.escaped(asASCII: false)
691+
if let escaped = us._escaped(asASCII: false) {
692+
result += escaped
693+
wantBreak = true
694+
} else if wantBreak && !hasBreak(between: result, and: us) {
695+
result += us.escaped(asASCII: true)
696+
wantBreak = true
697+
} else {
698+
result.unicodeScalars.append(us)
699+
wantBreak = false
700+
}
701+
}
702+
// Also prevent the last scalar from combining with the closing `"`.
703+
var suffix = "\"".unicodeScalars
704+
while !result.isEmpty {
705+
// Append first scalar of suffix, then check if it combines.
706+
result.unicodeScalars.append(suffix.first!)
707+
let i = result.index(before: result.endIndex)
708+
let j = result.unicodeScalars.index(before: result.endIndex)
709+
if i >= j {
710+
// All good; append the rest and we're done.
711+
result.unicodeScalars.append(contentsOf: suffix.dropFirst())
712+
break
713+
}
714+
// Cancel appending the scalar, then quote the last scalar in `result` and
715+
// prepend it to `suffix`.
716+
result.unicodeScalars.removeLast()
717+
let last = result.unicodeScalars.removeLast()
718+
suffix.insert(
719+
contentsOf: last.escaped(asASCII: true).unicodeScalars,
720+
at: suffix.startIndex)
682721
}
683-
result += "\""
684722
return result
685723
}
686724
}

stdlib/public/core/UnicodeScalar.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ extension Unicode.Scalar :
192192
/// ASCII characters; otherwise, pass `false`.
193193
/// - Returns: A string representation of the scalar.
194194
public func escaped(asASCII forceASCII: Bool) -> String {
195+
_escaped(asASCII: forceASCII) ?? String(self)
196+
}
197+
198+
internal func _escaped(asASCII forceASCII: Bool) -> String? {
195199
func lowNibbleAsHex(_ v: UInt32) -> String {
196200
let nibble = v & 15
197201
if nibble < 10 {
@@ -208,7 +212,7 @@ extension Unicode.Scalar :
208212
} else if self == "\"" {
209213
return "\\\""
210214
} else if _isPrintableASCII {
211-
return String(self)
215+
return nil
212216
} else if self == "\0" {
213217
return "\\0"
214218
} else if self == "\n" {
@@ -222,7 +226,7 @@ extension Unicode.Scalar :
222226
+ lowNibbleAsHex(UInt32(self) >> 4)
223227
+ lowNibbleAsHex(UInt32(self)) + "}"
224228
} else if !forceASCII {
225-
return String(self)
229+
return nil
226230
} else if UInt32(self) <= 0xFFFF {
227231
var result = "\\u{"
228232
result += lowNibbleAsHex(UInt32(self) >> 12)

test/stdlib/PrintString.swift

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,60 @@ PrintTests.test("Printable") {
2929

3030
let us1: UnicodeScalar = "\\"
3131
expectPrinted("\\", us1)
32-
expectEqual("\"\\\\\"", us1.description)
32+
expectEqual("\\", us1.description)
3333
expectDebugPrinted("\"\\\\\"", us1)
3434

3535
let us2: UnicodeScalar = ""
3636
expectPrinted("", us2)
37-
expectEqual("\"\"", us2.description)
37+
expectEqual("", us2.description)
3838
expectDebugPrinted("\"\\u{3042}\"", us2)
3939
}
4040

41-
PrintTests.test("Printable") {
41+
PrintTests.test("TrickyQuoting") {
42+
guard #available(SwiftStdlib 5.9, *) else { return }
43+
// U+301: COMBINING ACUTE ACCENT (Grapheme_Cluster_Break = Extend)
44+
let s1 = "\u{301}Foo"
45+
expectPrinted(s1, s1)
46+
expectDebugPrinted("\"\\u{0301}Foo\"", s1)
47+
48+
// U+302: COMBINING CIRCUMFLEX ACCENT (Grapheme_Cluster_Break = Extend)
49+
let s2 = "\u{301}\u{302}Foo"
50+
expectPrinted(s2, s2)
51+
expectDebugPrinted("\"\\u{0301}\\u{0302}Foo\"", s2)
52+
53+
let s3 = "Foo\n\u{301}\u{302}Foo"
54+
expectPrinted(s3, s3)
55+
expectDebugPrinted("\"Foo\\n\\u{0301}\\u{0302}Foo\"", s3)
56+
57+
// U+200D: ZERO WIDTH JOINER (Grapheme_Cluster_Break = ZWJ)
58+
let s4 = "\u{200d}Foo"
59+
expectPrinted(s4, s4)
60+
expectDebugPrinted("\"\\u{200D}Foo\"", s4)
61+
62+
// U+110BD: KAITHI NUMBER SIGN (Grapheme_Cluster_Break = Prepend)
63+
let s5 = "Foo\u{110BD}"
64+
expectPrinted(s5, s5)
65+
expectDebugPrinted("\"Foo\\u{000110BD}\"", s5)
66+
67+
// U+070F: SYRIAC ABBREVIATION MARK (Grapheme_Cluster_Break = Prepend)
68+
let s6 = "Foo\u{070F}\u{110BD}"
69+
expectPrinted(s6, s6)
70+
expectDebugPrinted("\"Foo\\u{070F}\\u{000110BD}\"", s6)
71+
72+
let s7 = "Foo\u{301}\u{070F}\u{110BD}"
73+
expectPrinted(s7, s7)
74+
expectDebugPrinted("\"Foo\u{301}\\u{070F}\\u{000110BD}\"", s7)
75+
76+
let s8 = "Foo\u{301}\u{302}\u{070F}\u{110BD}Foo"
77+
expectPrinted(s8, s8)
78+
expectDebugPrinted("\"Foo\u{0301}\u{0302}\u{070F}\u{110BD}Foo\"", s8)
79+
80+
let s9 = "Foo\u{301}"
81+
expectPrinted(s9, s9)
82+
expectDebugPrinted("\"Foo\u{0301}\"", s9)
83+
}
84+
85+
PrintTests.test("Optional") {
4286
expectPrinted("Optional(\"meow\")", String?("meow"))
4387
}
4488

0 commit comments

Comments
 (0)