-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Better debugDescription
for EncodingError
and DecodingError
#80941
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,6 +89,26 @@ 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. | ||
var errorPresentationDescription: String { | ||
if let intValue { | ||
"[\(intValue)]" | ||
} else { | ||
stringValue | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like a lone There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps, but an issue is that the description is also called by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not a great idea for any external entity (JSONDecoder here) to expect a certain format out of another value's If JSONDecoder wants a certain format when printing So, I still think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can make that change if you'd like: removing the changes to the description, and exposing the more concise version via a different variable.
|
||
} | ||
} | ||
|
||
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. | ||
var errorPresentationDescription: String { | ||
self | ||
.map { $0.errorPresentationDescription } // Can't use .map(\.errorPresentationDescription) due to https://github.com/swiftlang/swift/issues/80716 | ||
.joined(separator: "/") | ||
} | ||
} | ||
|
||
//===----------------------------------------------------------------------===// | ||
|
@@ -3724,6 +3744,64 @@ public enum DecodingError: Error { | |
} | ||
} | ||
|
||
@available(SwiftStdlib 6.2, *) | ||
extension EncodingError: CustomDebugStringConvertible { | ||
public var debugDescription: String { | ||
let (message, context) = switch self { | ||
case .invalidValue(let value, let context): | ||
( | ||
"Invalid value: \(String(reflecting: value)) (\(type(of: value)))", | ||
context | ||
) | ||
} | ||
|
||
var output = """ | ||
\(message) | ||
\(context.debugDescription) | ||
""" | ||
|
||
if let underlyingError = context.underlyingError { | ||
output.append("\nUnderlying error: \(underlyingError)") | ||
} | ||
if !context.codingPath.isEmpty { | ||
output.append("\nPath: \(context.codingPath.errorPresentationDescription)") | ||
} | ||
|
||
return output | ||
} | ||
} | ||
|
||
@available(SwiftStdlib 6.2, *) | ||
extension DecodingError: CustomDebugStringConvertible { | ||
public var debugDescription: String { | ||
let (message, context) = switch self { | ||
case .typeMismatch(let expectedType, let context): | ||
("Type mismatch: expected value of type \(expectedType).", context) | ||
case .valueNotFound(let expectedType, let context): | ||
("Expected value of type \(expectedType) but found null instead.", context) | ||
case .keyNotFound(let expectedKey, let context): | ||
("Key '\(expectedKey.errorPresentationDescription)' not found in keyed decoding container.", context) | ||
case .dataCorrupted(let context): | ||
("Data was corrupted.", context) | ||
} | ||
|
||
var output = message | ||
|
||
if !context.debugDescription.isEmpty { | ||
output.append("\nDebug description: \(context.debugDescription)") | ||
} | ||
|
||
if let underlyingError = context.underlyingError { | ||
output.append("\nUnderlying error: \(underlyingError)") | ||
} | ||
if !context.codingPath.isEmpty { | ||
output.append("\nPath: \(context.codingPath.errorPresentationDescription)") | ||
} | ||
|
||
return output | ||
} | ||
} | ||
|
||
// The following extensions allow for easier error construction. | ||
|
||
internal struct _GenericIndexKey: CodingKey, Sendable { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1100,6 +1100,237 @@ class TestCodable : TestCodableSuper { | |
expectRoundTripEqualityThroughPlist(for: UUIDCodingWrapper(uuid), lineNumber: testLine) | ||
} | ||
} | ||
|
||
// MARK: - DecodingError | ||
func expectErrorDescription( | ||
_ expectedErrorDescription: String, | ||
fromDecodingError error: DecodingError, | ||
lineNumber: UInt = #line | ||
) { | ||
expectEqual(String(describing: error), expectedErrorDescription, "Unexpectedly wrong error: \(error)", line: lineNumber) | ||
} | ||
|
||
func test_decodingError_typeMismatch_nilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Type mismatch: expected value of type String. | ||
Debug description: This is where the debug description goes | ||
Path: [0]/address/city/birds/[1]/name | ||
"""#, | ||
fromDecodingError: DecodingError.typeMismatch( | ||
String.self, | ||
DecodingError.Context( | ||
codingPath: [0, "address", "city", "birds", 1, "name"] as [GenericCodingKey], | ||
debugDescription: "This is where the debug description goes" | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_decodingError_typeMismatch_nonNilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Type mismatch: expected value of type String. | ||
Debug description: Some debug description | ||
Underlying error: GenericError(name: "some generic error goes here") | ||
Path: [0]/address/[1]/street | ||
"""#, | ||
fromDecodingError: DecodingError.typeMismatch( | ||
String.self, | ||
DecodingError.Context( | ||
codingPath: [0, "address", 1, "street"] as [GenericCodingKey], | ||
debugDescription: "Some debug description", | ||
underlyingError: GenericError(name: "some generic error goes here") | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_decodingError_valueNotFound_nilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Expected value of type String but found null instead. | ||
Debug description: Description for debugging purposes | ||
Path: [0]/firstName | ||
"""#, | ||
fromDecodingError: DecodingError.valueNotFound( | ||
String.self, | ||
DecodingError.Context( | ||
codingPath: [0, "firstName"] as [GenericCodingKey], | ||
debugDescription: "Description for debugging purposes" | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_decodingError_valueNotFound_nonNilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Expected value of type Int but found null instead. | ||
Debug description: Here is the debug description for value-not-found | ||
Underlying error: GenericError(name: "these aren\'t the droids you\'re looking for") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure where the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
Path: [0]/population | ||
"""#, | ||
fromDecodingError: DecodingError.valueNotFound( | ||
Int.self, | ||
DecodingError.Context( | ||
codingPath: [0, "population"] as [GenericCodingKey], | ||
debugDescription: "Here is the debug description for value-not-found", | ||
underlyingError: GenericError(name: "these aren't the droids you're looking for") | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_decodingError_keyNotFound_nilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Key 'name' not found in keyed decoding container. | ||
Debug description: How would you describe your relationship with your debugger? | ||
Path: [0]/address/city | ||
"""#, | ||
fromDecodingError: DecodingError.keyNotFound( | ||
GenericCodingKey(stringValue: "name"), | ||
DecodingError.Context( | ||
codingPath: [0, "address", "city"] as [GenericCodingKey], | ||
debugDescription: "How would you describe your relationship with your debugger?" | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_decodingError_keyNotFound_nonNilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Key 'name' not found in keyed decoding container. | ||
Debug description: Just some info to help you out | ||
Underlying error: GenericError(name: "hey, who turned out the lights?") | ||
Path: [0]/address/city | ||
"""#, | ||
fromDecodingError: DecodingError.keyNotFound( | ||
GenericCodingKey(stringValue: "name"), | ||
DecodingError.Context( | ||
codingPath: [0, "address", "city"] as [GenericCodingKey], | ||
debugDescription: "Just some info to help you out", | ||
underlyingError: GenericError(name: "hey, who turned out the lights?") | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_decodingError_dataCorrupted_emptyCodingPath() { | ||
expectErrorDescription( | ||
#""" | ||
Data was corrupted. | ||
Debug description: The given data was not valid JSON | ||
Underlying error: GenericError(name: "just some data corruption") | ||
"""#, | ||
fromDecodingError: DecodingError.dataCorrupted( | ||
DecodingError.Context( | ||
codingPath: [] as [GenericCodingKey], // sometimes empty when generated by JSONDecoder | ||
debugDescription: "The given data was not valid JSON", | ||
underlyingError: GenericError(name: "just some data corruption") | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_decodingError_dataCorrupted_nonEmptyCodingPath() { | ||
expectErrorDescription( | ||
#""" | ||
Data was corrupted. | ||
Debug description: There was apparently some data corruption! | ||
Underlying error: GenericError(name: "This data corruption is getting out of hand") | ||
Path: first/second/[2] | ||
"""#, | ||
fromDecodingError: DecodingError.dataCorrupted( | ||
DecodingError.Context( | ||
codingPath: ["first", "second", 2] as [GenericCodingKey], | ||
debugDescription: "There was apparently some data corruption!", | ||
underlyingError: GenericError(name: "This data corruption is getting out of hand") | ||
) | ||
) | ||
) | ||
} | ||
|
||
// MARK: - EncodingError | ||
func expectErrorDescription( | ||
_ expectedErrorDescription: String, | ||
fromEncodingError error: EncodingError, | ||
lineNumber: UInt = #line | ||
) { | ||
expectEqual(String(describing: error), expectedErrorDescription, "Unexpectedly wrong error: \(error)", line: lineNumber) | ||
} | ||
|
||
func test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Invalid value: 123 (Int) | ||
You cannot do that! | ||
"""#, | ||
fromEncodingError: EncodingError.invalidValue( | ||
123 as Int, | ||
EncodingError.Context( | ||
codingPath: [] as [GenericCodingKey], | ||
debugDescription: "You cannot do that!" | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Invalid value: 234 (Int) | ||
You cannot do that! | ||
Path: first/second/[2] | ||
"""#, | ||
fromEncodingError: EncodingError.invalidValue( | ||
234 as Int, | ||
EncodingError.Context( | ||
codingPath: ["first", "second", 2] as [GenericCodingKey], | ||
debugDescription: "You cannot do that!" | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Invalid value: 345 (Int) | ||
You cannot do that! | ||
Underlying error: GenericError(name: "You really cannot do that") | ||
"""#, | ||
fromEncodingError: EncodingError.invalidValue( | ||
345 as Int, | ||
EncodingError.Context( | ||
codingPath: [] as [GenericCodingKey], | ||
debugDescription: "You cannot do that!", | ||
underlyingError: GenericError(name: "You really cannot do that") | ||
) | ||
) | ||
) | ||
} | ||
|
||
func test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError() { | ||
expectErrorDescription( | ||
#""" | ||
Invalid value: 456 (Int) | ||
You cannot do that! | ||
Underlying error: GenericError(name: "You really cannot do that") | ||
Path: first/second/[2] | ||
"""#, | ||
fromEncodingError: EncodingError.invalidValue( | ||
456 as Int, | ||
EncodingError.Context( | ||
codingPath: ["first", "second", 2] as [GenericCodingKey], | ||
debugDescription: "You cannot do that!", | ||
underlyingError: GenericError(name: "You really cannot do that") | ||
) | ||
) | ||
) | ||
} | ||
} | ||
|
||
// MARK: - Helper Types | ||
|
@@ -1118,6 +1349,18 @@ struct GenericCodingKey: CodingKey { | |
} | ||
} | ||
|
||
extension GenericCodingKey: ExpressibleByStringLiteral { | ||
init(stringLiteral: String) { | ||
self.init(stringValue: stringLiteral) | ||
} | ||
} | ||
|
||
extension GenericCodingKey: ExpressibleByIntegerLiteral { | ||
init(integerLiteral: Int) { | ||
self.init(intValue: integerLiteral) | ||
} | ||
} | ||
|
||
struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable { | ||
let value: T | ||
|
||
|
@@ -1130,6 +1373,10 @@ struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable | |
} | ||
} | ||
|
||
struct GenericError: Error { | ||
let name: String | ||
} | ||
|
||
// MARK: - Tests | ||
|
||
#if !FOUNDATION_XCTEST | ||
|
@@ -1183,6 +1430,18 @@ var tests = [ | |
"test_URL_Plist" : TestCodable.test_URL_Plist, | ||
"test_UUID_JSON" : TestCodable.test_UUID_JSON, | ||
"test_UUID_Plist" : TestCodable.test_UUID_Plist, | ||
"test_decodingError_typeMismatch_nilUnderlyingError" : TestCodable.test_decodingError_typeMismatch_nilUnderlyingError, | ||
"test_decodingError_typeMismatch_nonNilUnderlyingError" : TestCodable.test_decodingError_typeMismatch_nonNilUnderlyingError, | ||
"test_decodingError_valueNotFound_nilUnderlyingError" : TestCodable.test_decodingError_valueNotFound_nilUnderlyingError, | ||
"test_decodingError_valueNotFound_nonNilUnderlyingError" : TestCodable.test_decodingError_valueNotFound_nonNilUnderlyingError, | ||
"test_decodingError_keyNotFound_nilUnderlyingError" : TestCodable.test_decodingError_keyNotFound_nilUnderlyingError, | ||
"test_decodingError_keyNotFound_nonNilUnderlyingError" : TestCodable.test_decodingError_keyNotFound_nonNilUnderlyingError, | ||
"test_decodingError_dataCorrupted_emptyCodingPath" : TestCodable.test_decodingError_dataCorrupted_emptyCodingPath, | ||
"test_decodingError_dataCorrupted_nonEmptyCodingPath" : TestCodable.test_decodingError_dataCorrupted_nonEmptyCodingPath, | ||
"test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError": TestCodable.test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError, | ||
"test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError": TestCodable.test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError, | ||
"test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError": TestCodable.test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError, | ||
"test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError": TestCodable.test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError, | ||
] | ||
|
||
#if os(macOS) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm starting to wonder whether we actually need the brackets. It's not like you'll ever be in a situation where you need help differentiating between keys and indices. This isn't PHP, for cryin' out loud. Thoughts on turning
[0]
into0
?