Skip to content
Open
126 changes: 126 additions & 0 deletions stdlib/public/core/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,58 @@ extension CodingKey {
public var debugDescription: String {
return description
}

/// A simplified description: the int value, if present, in square brackets.
/// Otherwise, the string value by itself. Used when concatenating coding keys
/// to form a path when printing debug information.
/// - parameter isFirst: Whether this is the first key in a coding path, in
/// which case we will omit the prepended '.' delimiter from string keys.
func errorPresentationDescription(isFirstInCodingPath isFirst: Bool = true) -> String {
if let intValue {
return "[\(intValue)]"
} else {
let delimiter = isFirst ? "" : "."
return "\(delimiter)\(stringValue.escapedForCodingKeyErrorPresentationDescription)"
}
}
}

private extension [any CodingKey] {
/// Concatenates the elements of an array of coding keys and joins them with
/// "/" separators to make them read like a path.
func errorPresentationDescription() -> String {
return (
self.prefix(1).map { $0.errorPresentationDescription(isFirstInCodingPath: true) }
+ self.dropFirst(1).map { $0.errorPresentationDescription(isFirstInCodingPath: false) }
).joined(separator: "")
}
}

extension String {
/// When printing coding paths, delimit string keys with a '.' (period). If
/// the key contains a period, escape it with backticks so that it can be
/// distinguished from the delimiter. Also escape backslashes and backticks
/// (but *not* periods) to avoid confusion with delimiters.
var escapedForCodingKeyErrorPresentationDescription: String {
let charactersThatNeedBackticks: Set<Character> = [".", "`", "\\"]
let charactersThatNeedEscaping: Set<Character> = ["`", "\\"]
assert(
charactersThatNeedEscaping.isSubset(of: charactersThatNeedBackticks),
"Only some characters in backticks will require further escaping to disambiguate them from the backticks"
)

var escaped = self
var needsBackticks = false
for (character, index) in zip(self, indices).reversed() {
if charactersThatNeedBackticks.contains(character) {
needsBackticks = true
if charactersThatNeedEscaping.contains(character) {
escaped.insert("\\", at: index)
}
}
}
return needsBackticks ? "`\(escaped)`" : self
}
}

//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -3724,6 +3776,80 @@ public enum DecodingError: Error {
}
}

@available(SwiftStdlib 6.3, *)
extension EncodingError: CustomDebugStringConvertible {
/// A textual representation of this encoding error, intended for debugging.
///
/// - Important: The contents of the returned string are not guaranteed to
/// remain stable: they may arbitrarily change in any Swift release.
@available(SwiftStdlib 6.3, *)
public var debugDescription: String {
let (message, context) = switch self {
case .invalidValue(let value, let context):
(
"EncodingError.invalidValue: \(String(reflecting: value)) (\(type(of: value)))",
context
)
}

var output = message

let contextDebugDescription = context.debugDescription

if !context.codingPath.isEmpty {
output.append(". Path: \(context.codingPath.errorPresentationDescription())")
}

if !contextDebugDescription.isEmpty {
output.append(". Debug description: \(context.debugDescription)")
}

if let underlyingError = context.underlyingError {
output.append(". Underlying error: \(underlyingError)")
}

return output
}
}

@available(SwiftStdlib 6.3, *)
extension DecodingError: CustomDebugStringConvertible {
/// A textual representation of this decoding error, intended for debugging.
///
/// - Important: The contents of the returned string are not guaranteed to
/// remain stable: they may arbitrarily change in any Swift release.
@available(SwiftStdlib 6.3, *)
public var debugDescription: String {
let (message, context) = switch self {
case .typeMismatch(let expectedType, let context):
("DecodingError.typeMismatch: expected value of type \(expectedType)", context)
case .valueNotFound(let expectedType, let context):
("DecodingError.valueNotFound: Expected value of type \(expectedType) but found null instead", context)
case .keyNotFound(let expectedKey, let context):
("DecodingError.keyNotFound: Key '\(expectedKey.errorPresentationDescription())' not found in keyed decoding container", context)
case .dataCorrupted(let context):
("DecodingError.dataCorrupted: Data was corrupted", context)
}

var output = message

if !context.codingPath.isEmpty {
output.append(". Path: \(context.codingPath.errorPresentationDescription())")
}

let contextDebugDescription = context.debugDescription
if !contextDebugDescription.isEmpty {
output.append(". Debug description: \(contextDebugDescription)")
}

if let underlyingError = context.underlyingError {
output.append(". Underlying error: \(underlyingError)")
}

return output
}
}

// The following extensions allow for easier error construction.

internal struct _GenericIndexKey: CodingKey, Sendable {
Expand Down
2 changes: 1 addition & 1 deletion test/Macros/macro_plugin_error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func test() {
// FIXME: -module-abi-name ABI name is leaking.

let _: String = #fooMacro(1)
// expected-error @-1 {{typeMismatch(_CompilerSwiftCompilerPluginMessageHandling.PluginToHostMessage}}
// expected-error @-1 {{typeMismatch}}
let _: String = #fooMacro(2)
// expected-error @-1 {{failed to receive result from plugin (from macro 'fooMacro')}}
let _: String = #fooMacro(3)
Expand Down
10 changes: 10 additions & 0 deletions test/abi/macOS/arm64/stdlib.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1148,3 +1148,13 @@ Added: __swift_debug_metadataAllocatorPageSize
Added: __swift_retainRelease_slowpath_mask_v1
Added: _swift_release_preservemost
Added: _swift_retain_preservemost

// SE-0489 Better debugDescription for EncodingError and DecodingError
Added: _$ss13DecodingErrorO16debugDescriptionSSvg
Added: _$ss13DecodingErrorO16debugDescriptionSSvpMV
Added: _$ss13DecodingErrorOs28CustomDebugStringConvertiblesMc
Added: _$ss13DecodingErrorOs28CustomDebugStringConvertiblesWP
Added: _$ss13EncodingErrorO16debugDescriptionSSvg
Added: _$ss13EncodingErrorO16debugDescriptionSSvpMV
Added: _$ss13EncodingErrorOs28CustomDebugStringConvertiblesMc
Added: _$ss13EncodingErrorOs28CustomDebugStringConvertiblesWP
10 changes: 10 additions & 0 deletions test/abi/macOS/x86_64/stdlib.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1143,3 +1143,13 @@ Removed: _$sSS10_wordIndex6beforeSS0B0VAD_tF
// Internal info exposed for swift-inspect.
Added: __swift_debug_allocationPoolSize
Added: __swift_debug_metadataAllocatorPageSize

// SE-0489 Better debugDescription for EncodingError and DecodingError
Added: _$ss13DecodingErrorO16debugDescriptionSSvg
Added: _$ss13DecodingErrorO16debugDescriptionSSvpMV
Added: _$ss13DecodingErrorOs28CustomDebugStringConvertiblesMc
Added: _$ss13DecodingErrorOs28CustomDebugStringConvertiblesWP
Added: _$ss13EncodingErrorO16debugDescriptionSSvg
Added: _$ss13EncodingErrorO16debugDescriptionSSvpMV
Added: _$ss13EncodingErrorOs28CustomDebugStringConvertiblesMc
Added: _$ss13EncodingErrorOs28CustomDebugStringConvertiblesWP
4 changes: 4 additions & 0 deletions test/api-digester/stability-stdlib-abi-without-asserts.test
Original file line number Diff line number Diff line change
Expand Up @@ -879,4 +879,8 @@ Func _float64ToStringImpl(_:_:_:_:) is a new API without '@available'
Func _int64ToStringImpl(_:_:_:_:_:) is a new API without '@available'
Func _uint64ToStringImpl(_:_:_:_:_:) is a new API without '@available'

// New conformances from SE-0489: Better debugDescription for EncodingError and DecodingError
Enum DecodingError has added a conformance to an existing protocol CustomDebugStringConvertible
Enum EncodingError has added a conformance to an existing protocol CustomDebugStringConvertible

// *** DO NOT DISABLE OR XFAIL THIS TEST. *** (See comment above.)
Loading