mirror of
https://github.com/apple/swift.git
synced 2026-02-27 18:26:24 +01:00
Now accepted as [SE-0489](https://github.com/ZevEisenberg/swift-evolution/blob/main/proposals/0489-codable-error-printing.md). # To Do - [x] confirm which version of Swift to use for the availability annotations. Probably 6.3 at time of writing. # Context Re: [Swift forum post](https://forums.swift.org/t/the-future-of-serialization-deserialization-apis/78585/77), where a discussion about future serialization tools in Swift prompted Kevin Perry to suggest that some proposed changes could actually be made in today's stdlib. # Summary of Changes Conforms `EncodingError` and `DecodingError` to `CustomDebugStringConvertible` and adds a more-readable `debugDescription`. # Future Directions This is a pared-down version of some experiments I did in [UsefulDecode](https://github.com/ZevEisenberg/UsefulDecode). The changes in this PR are the best I could do without changing the public interface of `DecodingError` and `EncodingError`, and without modifying the way the `JSON`/`PropertyList` `Encoder`/`Decoder` in Foundation generate their errors' debug descriptions. In the above-linked [UsefulDecode](https://github.com/ZevEisenberg/UsefulDecode) repo, when JSON decoding fails, I go back and re-decode the JSON using `JSONSerialization` in order to provide more context about what failed, and why. I didn't attempt to make such a change here, but I'd like to discuss what may be possible. # Examples To illustrate the effect of the changes in this PR, I removed my changes to stdlib/public/core/Codable.swift and ran my new test cases again. Here are the resulting diffs. ## `test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError` ### Before `invalidValue(234, Swift.EncodingError.Context(codingPath: [GenericCodingKey(stringValue: "first", intValue: nil), GenericCodingKey(stringValue: "second", intValue: nil), GenericCodingKey(stringValue: "2", intValue: 2)], debugDescription: "You cannot do that!", underlyingError: nil))` ### After `EncodingError.invalidValue: 234 (Int). Path: first.second[2]. Debug description: You cannot do that!` ## `test_decodingError_valueNotFound_nilUnderlyingError` ### Before `valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "firstName", intValue: nil)], debugDescription: "Description for debugging purposes", underlyingError: nil))` ### After `DecodingError.valueNotFound: Expected value of type String but found null instead. Path: [0].firstName. Debug description: Description for debugging purposes` ## `test_decodingError_keyNotFound_nonNilUnderlyingError` ### Before `keyNotFound(GenericCodingKey(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "city", intValue: nil)], debugDescription: "Just some info to help you out", underlyingError: Optional(main.GenericError(name: "hey, who turned out the lights?"))))` ### After `DecodingError.keyNotFound: Key \'name\' not found in keyed decoding container. Path: [0].address.city. Debug description: Just some info to help you out. Underlying error: GenericError(name: "hey, who turned out the lights?")` ## `test_decodingError_typeMismatch_nilUnderlyingError` ### Before `typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "city", intValue: nil), GenericCodingKey(stringValue: "birds", intValue: nil), GenericCodingKey(stringValue: "1", intValue: 1), GenericCodingKey(stringValue: "name", intValue: nil)], debugDescription: "This is where the debug description goes", underlyingError: nil))` ### After `DecodingError.typeMismatch: expected value of type String. Path: [0].address.city.birds[1].name. Debug description: This is where the debug description goes` ## `test_decodingError_dataCorrupted_nonEmptyCodingPath` ### Before `dataCorrupted(Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "first", intValue: nil), GenericCodingKey(stringValue: "second", intValue: nil), GenericCodingKey(stringValue: "2", intValue: 2)], debugDescription: "There was apparently some data corruption!", underlyingError: Optional(main.GenericError(name: "This data corruption is getting out of hand"))))` ### After `DecodingError.dataCorrupted: Data was corrupted. Path: first.second[2]. Debug description: There was apparently some data corruption!. Underlying error: GenericError(name: "This data corruption is getting out of hand")` ## `test_decodingError_valueNotFound_nonNilUnderlyingError` ### Before `valueNotFound(Swift.Int, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "population", intValue: nil)], debugDescription: "Here is the debug description for value-not-found", underlyingError: Optional(main.GenericError(name: "these aren\\\'t the droids you\\\'re looking for"))))` ### After `DecodingError.valueNotFound: Expected value of type Int but found null instead. Path: [0].population. Debug description: Here is the debug description for value-not-found. Underlying error: GenericError(name: "these aren\\\'t the droids you\\\'re looking for")` ## `test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError` ### Before `invalidValue(345, Swift.EncodingError.Context(codingPath: [], debugDescription: "You cannot do that!", underlyingError: Optional(main.GenericError(name: "You really cannot do that"))))` ### After `EncodingError.invalidValue: 345 (Int). Debug description: You cannot do that!. Underlying error: GenericError(name: "You really cannot do that")` ## `test_decodingError_typeMismatch_nonNilUnderlyingError` ### Before `typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "1", intValue: 1), GenericCodingKey(stringValue: "street", intValue: nil)], debugDescription: "Some debug description", underlyingError: Optional(main.GenericError(name: "some generic error goes here"))))` ### After `DecodingError.typeMismatch: expected value of type String. Path: [0].address[1].street. Debug description: Some debug description. Underlying error: GenericError(name: "some generic error goes here")` ## `test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError` ### Before `invalidValue(123, Swift.EncodingError.Context(codingPath: [], debugDescription: "You cannot do that!", underlyingError: nil))` ### After `EncodingError.invalidValue: 123 (Int). Debug description: You cannot do that!` ## `test_decodingError_keyNotFound_nilUnderlyingError` ### Before `keyNotFound(GenericCodingKey(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "city", intValue: nil)], debugDescription: "How would you describe your relationship with your debugger?", underlyingError: nil))` ### After `DecodingError.keyNotFound: Key \'name\' not found in keyed decoding container. Path: [0]address.city. Debug description: How would you describe your relationship with your debugger?` ## `test_decodingError_dataCorrupted_emptyCodingPath` ### Before `dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON", underlyingError: Optional(main.GenericError(name: "just some data corruption"))))` ### After `DecodingError.dataCorrupted: Data was corrupted. Debug description: The given data was not valid JSON. Underlying error: GenericError(name: "just some data corruption")` ## `test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError` ### Before `invalidValue(456, Swift.EncodingError.Context(codingPath: [GenericCodingKey(stringValue: "first", intValue: nil), GenericCodingKey(stringValue: "second", intValue: nil), GenericCodingKey(stringValue: "2", intValue: 2)], debugDescription: "You cannot do that!", underlyingError: Optional(main.GenericError(name: "You really cannot do that"))))` ### After `EncodingError.invalidValue: 456 (Int). Path: first.second[2]. Debug description: You cannot do that!. Underlying error: GenericError(name: "You really cannot do that")`
72 lines
3.9 KiB
Swift
72 lines
3.9 KiB
Swift
// REQUIRES: swift_swift_parser
|
|
// REQUIRES: swift_feature_Macros
|
|
|
|
// RUN: %empty-directory(%t)
|
|
// RUN: split-file %s %t
|
|
|
|
// RUN: %swift-build-c-plugin -o %t/mock-plugin %t/plugin.c
|
|
|
|
// RUN: env SWIFT_DUMP_PLUGIN_MESSAGING=1 %target-swift-frontend \
|
|
// RUN: -typecheck -verify \
|
|
// RUN: -swift-version 5 -enable-experimental-feature Macros \
|
|
// RUN: -load-plugin-executable %t/mock-plugin#TestPlugin \
|
|
// RUN: -module-name MyApp \
|
|
// RUN: %t/test.swift \
|
|
// RUN: > %t/macro-expansions.txt 2>&1
|
|
|
|
// RUN: %FileCheck -strict-whitespace %s < %t/macro-expansions.txt
|
|
|
|
// CHECK: ->(plugin:[[#PID1:]]) {"getCapability":{"capability":{"protocolVersion":[[#PROTOCOL_VERSION:]]}}}
|
|
// CHECK-NEXT: <-(plugin:[[#PID1]]) {"getCapabilityResult":{"capability":{"protocolVersion":1}}}
|
|
// CHECK-NEXT: ->(plugin:[[#PID1]]) {"expandFreestandingMacro":{"discriminator":"$s{{.+}}","lexicalContext":[{{.*}}],"macro":{"moduleName":"TestPlugin","name":"fooMacro","typeName":"FooMacro"},"macroRole":"expression","staticBuildConfiguration"{{.*}},"syntax":{"kind":"expression","location":{"column":19,"fileID":"MyApp/test.swift","fileName":"{{.+}}test.swift","line":7,"offset":[[#]]},"source":"#fooMacro(1)"}}}
|
|
// CHECK-NEXT: <-(plugin:[[#PID1]]) {"invalidResponse":{}}
|
|
// CHECK-NEXT: ->(plugin:[[#PID1]]) {"expandFreestandingMacro":{"discriminator":"$s{{.+}}","lexicalContext":[{{.*}}],"macro":{"moduleName":"TestPlugin","name":"fooMacro","typeName":"FooMacro"},"macroRole":"expression","staticBuildConfiguration"{{.*}},"syntax":{"kind":"expression","location":{"column":19,"fileID":"MyApp/test.swift","fileName":"{{.+}}test.swift","line":9,"offset":[[#]]},"source":"#fooMacro(2)"}}}
|
|
// ^ This messages causes the mock plugin exit because there's no matching expected message.
|
|
|
|
// CHECK: ->(plugin:[[#PID2:]]) {"getCapability":{"capability":{"protocolVersion":[[#PROTOCOL_VERSION]]}}}
|
|
// CHECK-NEXT: <-(plugin:[[#PID2]]) {"getCapabilityResult":{"capability":{"protocolVersion":1}}}
|
|
// CHECK-NEXT: ->(plugin:[[#PID2]]) {"expandFreestandingMacro":{"discriminator":"$s{{.+}}","lexicalContext":[{{.*}}],"macro":{"moduleName":"TestPlugin","name":"fooMacro","typeName":"FooMacro"},"macroRole":"expression","staticBuildConfiguration"{{.*}},"syntax":{"kind":"expression","location":{"column":19,"fileID":"MyApp/test.swift","fileName":"{{.+}}test.swift","line":11,"offset":[[#]]},"source":"#fooMacro(3)"}}}
|
|
// CHECK-NEXT: <-(plugin:[[#PID2:]]) {"expandFreestandingMacroResult":{"diagnostics":[],"expandedSource":"3.description"}}
|
|
// CHECK-NEXT: ->(plugin:[[#PID2:]]) {{$}}
|
|
|
|
//--- test.swift
|
|
@freestanding(expression) macro fooMacro(_: Any) -> String = #externalMacro(module: "TestPlugin", type: "FooMacro")
|
|
|
|
func test() {
|
|
// FIXME: Should be more user friendly message.
|
|
// FIXME: -module-abi-name ABI name is leaking.
|
|
|
|
let _: String = #fooMacro(1)
|
|
// expected-error @-1 {{typeMismatch}}
|
|
let _: String = #fooMacro(2)
|
|
// expected-error @-1 {{failed to receive result from plugin (from macro 'fooMacro')}}
|
|
let _: String = #fooMacro(3)
|
|
// ^ This should succeed.
|
|
}
|
|
|
|
//--- plugin.c
|
|
#include "swift-c/MockPlugin/MockPlugin.h"
|
|
|
|
MOCK_PLUGIN([
|
|
{
|
|
"expect": {"getCapability": {}},
|
|
"response": {"getCapabilityResult": {"capability": {"protocolVersion": 1}}}
|
|
},
|
|
{
|
|
"expect": {"expandFreestandingMacro": {
|
|
"macro": {"moduleName": "TestPlugin", "typeName": "FooMacro"},
|
|
"syntax": {"kind": "expression", "source": "#fooMacro(1)"}}},
|
|
"response": {"invalidResponse": {}}
|
|
},
|
|
{
|
|
"expect": {"getCapability": {}},
|
|
"response": {"getCapabilityResult": {"capability": {"protocolVersion": 1}}}
|
|
},
|
|
{
|
|
"expect": {"expandFreestandingMacro": {
|
|
"macro": {"moduleName": "TestPlugin", "typeName": "FooMacro"},
|
|
"syntax": {"kind": "expression", "source": "#fooMacro(3)"}}},
|
|
"response": {"expandFreestandingMacroResult": {"expandedSource": "3.description", "diagnostics": []}}
|
|
}
|
|
])
|