Skip to content

[stdlib] String.debugDescription: Fix quoting behavior #63048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions stdlib/public/core/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -676,11 +676,49 @@ extension String: ExpressibleByStringLiteral {
extension String: CustomDebugStringConvertible {
/// A representation of the string that is suitable for debugging.
public var debugDescription: String {
func hasBreak(between left: String, and right: Unicode.Scalar) -> Bool {
// Note: we know `left` ends with an ASCII character, so we only need to
// look at its last scalar.
var state = _GraphemeBreakingState()
return state.shouldBreak(between: left.unicodeScalars.last!, and: right)
}

// Prevent unquoted scalars in the string from combining with the opening
// `"` or the tail of the preceding quoted scalar.
var result = "\""
var wantBreak = true // true if next scalar must not combine with the last
for us in self.unicodeScalars {
result += us.escaped(asASCII: false)
if let escaped = us._escaped(asASCII: false) {
result += escaped
wantBreak = true
} else if wantBreak && !hasBreak(between: result, and: us) {
result += us.escaped(asASCII: true)
wantBreak = true
} else {
result.unicodeScalars.append(us)
wantBreak = false
}
}
// Also prevent the last scalar from combining with the closing `"`.
var suffix = "\"".unicodeScalars
while !result.isEmpty {
// Append first scalar of suffix, then check if it combines.
result.unicodeScalars.append(suffix.first!)
let i = result.index(before: result.endIndex)
let j = result.unicodeScalars.index(before: result.endIndex)
if i >= j {
// All good; append the rest and we're done.
result.unicodeScalars.append(contentsOf: suffix.dropFirst())
break
}
// Cancel appending the scalar, then quote the last scalar in `result` and
// prepend it to `suffix`.
result.unicodeScalars.removeLast()
let last = result.unicodeScalars.removeLast()
suffix.insert(
contentsOf: last.escaped(asASCII: true).unicodeScalars,
at: suffix.startIndex)
}
result += "\""
return result
}
}
Expand Down
8 changes: 6 additions & 2 deletions stdlib/public/core/UnicodeScalar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ extension Unicode.Scalar :
/// ASCII characters; otherwise, pass `false`.
/// - Returns: A string representation of the scalar.
public func escaped(asASCII forceASCII: Bool) -> String {
_escaped(asASCII: forceASCII) ?? String(self)
}

internal func _escaped(asASCII forceASCII: Bool) -> String? {
func lowNibbleAsHex(_ v: UInt32) -> String {
let nibble = v & 15
if nibble < 10 {
Expand All @@ -208,7 +212,7 @@ extension Unicode.Scalar :
} else if self == "\"" {
return "\\\""
} else if _isPrintableASCII {
return String(self)
return nil
} else if self == "\0" {
return "\\0"
} else if self == "\n" {
Expand All @@ -222,7 +226,7 @@ extension Unicode.Scalar :
+ lowNibbleAsHex(UInt32(self) >> 4)
+ lowNibbleAsHex(UInt32(self)) + "}"
} else if !forceASCII {
return String(self)
return nil
} else if UInt32(self) <= 0xFFFF {
var result = "\\u{"
result += lowNibbleAsHex(UInt32(self) >> 12)
Expand Down
50 changes: 47 additions & 3 deletions test/stdlib/PrintString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,60 @@ PrintTests.test("Printable") {

let us1: UnicodeScalar = "\\"
expectPrinted("\\", us1)
expectEqual("\"\\\\\"", us1.description)
expectEqual("\\", us1.description)
expectDebugPrinted("\"\\\\\"", us1)

let us2: UnicodeScalar = "あ"
expectPrinted("あ", us2)
expectEqual("\"あ\"", us2.description)
expectEqual("", us2.description)
expectDebugPrinted("\"\\u{3042}\"", us2)
}

PrintTests.test("Printable") {
PrintTests.test("TrickyQuoting") {
guard #available(SwiftStdlib 5.9, *) else { return }
// U+301: COMBINING ACUTE ACCENT (Grapheme_Cluster_Break = Extend)
let s1 = "\u{301}Foo"
expectPrinted(s1, s1)
expectDebugPrinted("\"\\u{0301}Foo\"", s1)

// U+302: COMBINING CIRCUMFLEX ACCENT (Grapheme_Cluster_Break = Extend)
let s2 = "\u{301}\u{302}Foo"
expectPrinted(s2, s2)
expectDebugPrinted("\"\\u{0301}\\u{0302}Foo\"", s2)

let s3 = "Foo\n\u{301}\u{302}Foo"
expectPrinted(s3, s3)
expectDebugPrinted("\"Foo\\n\\u{0301}\\u{0302}Foo\"", s3)

// U+200D: ZERO WIDTH JOINER (Grapheme_Cluster_Break = ZWJ)
let s4 = "\u{200d}Foo"
expectPrinted(s4, s4)
expectDebugPrinted("\"\\u{200D}Foo\"", s4)

// U+110BD: KAITHI NUMBER SIGN (Grapheme_Cluster_Break = Prepend)
let s5 = "Foo\u{110BD}"
expectPrinted(s5, s5)
expectDebugPrinted("\"Foo\\u{000110BD}\"", s5)

// U+070F: SYRIAC ABBREVIATION MARK (Grapheme_Cluster_Break = Prepend)
let s6 = "Foo\u{070F}\u{110BD}"
expectPrinted(s6, s6)
expectDebugPrinted("\"Foo\\u{070F}\\u{000110BD}\"", s6)

let s7 = "Foo\u{301}\u{070F}\u{110BD}"
expectPrinted(s7, s7)
expectDebugPrinted("\"Foo\u{301}\\u{070F}\\u{000110BD}\"", s7)

let s8 = "Foo\u{301}\u{302}\u{070F}\u{110BD}Foo"
expectPrinted(s8, s8)
expectDebugPrinted("\"Foo\u{0301}\u{0302}\u{070F}\u{110BD}Foo\"", s8)

let s9 = "Foo\u{301}"
expectPrinted(s9, s9)
expectDebugPrinted("\"Foo\u{0301}\"", s9)
}

PrintTests.test("Optional") {
expectPrinted("Optional(\"meow\")", String?("meow"))
}

Expand Down