mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
[SE-0112] Add error protocols LocalizedError, RecoverableError, CustomNSError
An error type can conform to one or more of these new protocols to customize its behavior and representation. From an implementation standpoint, the protocol conformances are used to fill in the user-info dictionary in NSError to interoperate with the Cocoa error-handling system. There are a few outstanding problems with this implementation, although it is fully functional: * Population of the userInfo dictionary is currently eager; we should use user info providers on platforms where they are available. * At present, the Swift dynamic casting machinery is unable to unbox a _SwiftNativeNSError when trying to cast from it to (e.g.) an existential, which makes it impossible to retrieve the RecoverableError from the NSError. Instead, just capture the original error---hey, they're supposed to be value types anyway!---and use that to implement the entry points for the informal NSErrorRecoveryAttempting protocol. This is part (1) of the proposal solution.
This commit is contained in:
@@ -13,6 +13,177 @@
|
||||
import CoreFoundation
|
||||
import Darwin
|
||||
|
||||
/// Describes an error that provides localized messages describing why
|
||||
/// an error occurred and provides more information about the error.
|
||||
public protocol LocalizedError : ErrorProtocol {
|
||||
/// A localized message describing what error occurred.
|
||||
var errorDescription: String? { get }
|
||||
|
||||
/// A localized message describing the reason for the failure.
|
||||
var failureReason: String? { get }
|
||||
|
||||
/// A localized message describing how one might recover from the failure.
|
||||
var recoverySuggestion: String? { get }
|
||||
|
||||
/// A localized message providing "help" text if the user requests help.
|
||||
var helpAnchor: String? { get }
|
||||
}
|
||||
|
||||
public extension LocalizedError {
|
||||
var errorDescription: String? { return nil }
|
||||
var failureReason: String? { return nil }
|
||||
var recoverySuggestion: String? { return nil }
|
||||
var helpAnchor: String? { return nil }
|
||||
}
|
||||
|
||||
@_silgen_name("NS_Swift_performErrorRecoverySelector")
|
||||
internal func NS_Swift_performErrorRecoverySelector(
|
||||
delegate: AnyObject?,
|
||||
selector: Selector,
|
||||
success: ObjCBool,
|
||||
contextInfo: UnsafeMutablePointer<Void>?)
|
||||
|
||||
/// Class that implements the informal protocol
|
||||
/// NSErrorRecoveryAttempting, which is used by NSError when it
|
||||
/// attempts recovery from an error.
|
||||
class _NSErrorRecoveryAttempter {
|
||||
// FIXME: If we could meaningfully cast the nsError back to RecoverableError,
|
||||
// we wouldn't need to capture this and could use the user-info
|
||||
// domain providers even for recoverable errors.
|
||||
let error: RecoverableError
|
||||
|
||||
init(error: RecoverableError) {
|
||||
self.error = error
|
||||
}
|
||||
|
||||
@objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
|
||||
func attemptRecovery(fromError nsError: NSError,
|
||||
optionIndex recoveryOptionIndex: Int,
|
||||
delegate: AnyObject?,
|
||||
didRecoverSelector: Selector,
|
||||
contextInfo: UnsafeMutablePointer<Void>?) {
|
||||
error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in
|
||||
NS_Swift_performErrorRecoverySelector(
|
||||
delegate: delegate,
|
||||
selector: didRecoverSelector,
|
||||
success: ObjCBool(success),
|
||||
contextInfo: contextInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(attemptRecoveryFromError:optionIndex:)
|
||||
func attemptRecovery(fromError nsError: NSError,
|
||||
optionIndex recoveryOptionIndex: Int) -> Bool {
|
||||
return error.attemptRecovery(optionIndex: recoveryOptionIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes an error that may be recoverably by presenting several
|
||||
/// potential recovery options to the user.
|
||||
public protocol RecoverableError : ErrorProtocol {
|
||||
/// Provides a set of possible recovery options to present to the user.
|
||||
var recoveryOptions: [String] { get }
|
||||
|
||||
/// Attempt to recover from this error when the user selected the
|
||||
/// option at the given index. This routine must call resultHandler and
|
||||
/// indicate whether recovery was successful (or not).
|
||||
///
|
||||
/// This entry point is used for recovery of errors handled at a
|
||||
/// "document" granularity, that do not affect the entire
|
||||
/// application.
|
||||
func attemptRecovery(optionIndex recoveryOptionIndex: Int,
|
||||
andThen resultHandler: (recovered: Bool) -> Void)
|
||||
|
||||
/// Attempt to recover from this error when the user selected the
|
||||
/// option at the given index. Returns true to indicate
|
||||
/// successful recovery, and false otherwise.
|
||||
///
|
||||
/// This entry point is used for recovery of errors handled at
|
||||
/// the "application" granularity, where nothing else in the
|
||||
/// application can proceed until the attmpted error recovery
|
||||
/// completes.
|
||||
func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
|
||||
}
|
||||
|
||||
public extension RecoverableError {
|
||||
/// By default, implements document-modal recovery via application-model
|
||||
/// recovery.
|
||||
func attemptRecovery(optionIndex recoveryOptionIndex: Int,
|
||||
andThen resultHandler: (recovered: Bool) -> Void) {
|
||||
resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes an error type that specifically provides a domain, code,
|
||||
/// and user-info dictionary.
|
||||
public protocol CustomNSError : ErrorProtocol {
|
||||
/// The domain of the error.
|
||||
var errorDomain: String { get }
|
||||
|
||||
/// The error code within the given domain.
|
||||
var errorCode: Int { get }
|
||||
|
||||
/// The user-info dictionary.
|
||||
var errorUserInfo: [String : AnyObject] { get }
|
||||
}
|
||||
|
||||
public extension ErrorProtocol where Self : CustomNSError {
|
||||
/// Default implementation for customized NSErrors.
|
||||
var _domain: String { return self.errorDomain }
|
||||
|
||||
/// Default implementation for customized NSErrors.
|
||||
var _code: Int { return self.errorCode }
|
||||
}
|
||||
|
||||
/// Retrieve the default userInfo dictionary for a given error.
|
||||
@_silgen_name("swift_Foundation_getErrorDefaultUserInfo")
|
||||
public func _swift_Foundation_getErrorDefaultUserInfo(_ error: ErrorProtocol)
|
||||
-> AnyObject? {
|
||||
// If the OS supports value user info value providers, use those
|
||||
// to lazily populate the user-info dictionary for this domain.
|
||||
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
|
||||
// FIXME: This is not implementable until we can recover the
|
||||
// original error from an NSError.
|
||||
}
|
||||
|
||||
// Populate the user-info dictionary
|
||||
var result: [String : AnyObject]
|
||||
|
||||
// Initialize with custom user-info.
|
||||
if let customNSError = error as? CustomNSError {
|
||||
result = customNSError.errorUserInfo
|
||||
} else {
|
||||
result = [:]
|
||||
}
|
||||
|
||||
if let localizedError = error as? LocalizedError {
|
||||
if let description = localizedError.errorDescription {
|
||||
result[NSLocalizedDescriptionKey] = description as AnyObject
|
||||
}
|
||||
|
||||
if let reason = localizedError.failureReason {
|
||||
result[NSLocalizedFailureReasonErrorKey] = reason as AnyObject
|
||||
}
|
||||
|
||||
if let suggestion = localizedError.recoverySuggestion {
|
||||
result[NSLocalizedRecoverySuggestionErrorKey] = suggestion as AnyObject
|
||||
}
|
||||
|
||||
if let helpAnchor = localizedError.helpAnchor {
|
||||
result[NSHelpAnchorErrorKey] = helpAnchor as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
if let recoverableError = error as? RecoverableError {
|
||||
result[NSLocalizedRecoveryOptionsErrorKey] =
|
||||
recoverableError.recoveryOptions as AnyObject
|
||||
result[NSRecoveryAttempterErrorKey] =
|
||||
_NSErrorRecoveryAttempter(error: recoverableError)
|
||||
}
|
||||
|
||||
return result as AnyObject
|
||||
}
|
||||
|
||||
// NSError and CFError conform to the standard ErrorProtocol protocol. Compiler
|
||||
// magic allows this to be done as a "toll-free" conversion when an NSError
|
||||
// or CFError is used as an ErrorProtocol existential.
|
||||
@@ -20,6 +191,7 @@ import Darwin
|
||||
extension NSError : ErrorProtocol {
|
||||
public var _domain: String { return domain }
|
||||
public var _code: Int { return code }
|
||||
public var _userInfo: AnyObject? { return userInfo as AnyObject }
|
||||
}
|
||||
|
||||
extension CFError : ErrorProtocol {
|
||||
@@ -30,6 +202,10 @@ extension CFError : ErrorProtocol {
|
||||
public var _code: Int {
|
||||
return CFErrorGetCode(self)
|
||||
}
|
||||
|
||||
public var _userInfo: AnyObject? {
|
||||
return CFErrorCopyUserInfo(self) as AnyObject?
|
||||
}
|
||||
}
|
||||
|
||||
// An error value to use when an Objective-C API indicates error
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <objc/message.h>
|
||||
|
||||
#include "swift/Runtime/Config.h"
|
||||
|
||||
@@ -118,3 +119,12 @@ NS_Swift_NSKeyedUnarchiver_unarchiveObjectWithData(
|
||||
return [result retain];
|
||||
}
|
||||
|
||||
// -- NSError
|
||||
SWIFT_CC(swift)
|
||||
extern "C" void
|
||||
NS_Swift_performErrorRecoverySelector(_Nullable id delegate,
|
||||
SEL selector,
|
||||
BOOL success,
|
||||
void * _Nullable contextInfo) {
|
||||
objc_msgSend(delegate, selector, success, contextInfo);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
import SwiftShims
|
||||
|
||||
// TODO: API review
|
||||
/// A type representing an error value that can be thrown.
|
||||
@@ -111,17 +112,12 @@
|
||||
public protocol ErrorProtocol {
|
||||
var _domain: String { get }
|
||||
var _code: Int { get }
|
||||
}
|
||||
|
||||
extension ErrorProtocol {
|
||||
public var _domain: String {
|
||||
return String(reflecting: self.dynamicType)
|
||||
}
|
||||
var _userInfo: AnyObject? { get }
|
||||
}
|
||||
|
||||
#if _runtime(_ObjC)
|
||||
// Helper functions for the C++ runtime to have easy access to domain and
|
||||
// code as Objective-C values.
|
||||
// Helper functions for the C++ runtime to have easy access to domain,
|
||||
// code, and userInfo as Objective-C values.
|
||||
@_silgen_name("swift_stdlib_getErrorDomainNSString")
|
||||
public func _stdlib_getErrorDomainNSString<T : ErrorProtocol>(_ x: UnsafePointer<T>)
|
||||
-> AnyObject {
|
||||
@@ -133,6 +129,17 @@ public func _stdlib_getErrorCode<T : ErrorProtocol>(_ x: UnsafePointer<T>) -> In
|
||||
return x.pointee._code
|
||||
}
|
||||
|
||||
// Helper functions for the C++ runtime to have easy access to domain and
|
||||
// code as Objective-C values.
|
||||
@_silgen_name("swift_stdlib_getErrorUserInfoNSDictionary")
|
||||
public func _stdlib_getErrorUserInfoNSDictionary<T : ErrorProtocol>(_ x: UnsafePointer<T>)
|
||||
-> AnyObject? {
|
||||
return x.pointee._userInfo
|
||||
}
|
||||
|
||||
@_silgen_name("swift_stdlib_getErrorDefaultUserInfo")
|
||||
public func _stdlib_getErrorDefaultUserInfo(_ error: ErrorProtocol) -> AnyObject?
|
||||
|
||||
// Known function for the compiler to use to coerce `ErrorProtocol` instances
|
||||
// to `NSError`.
|
||||
@_silgen_name("swift_bridgeErrorProtocolToNSError")
|
||||
@@ -154,3 +161,17 @@ public func _errorInMain(_ error: ErrorProtocol) {
|
||||
|
||||
@available(*, unavailable, renamed: "ErrorProtocol")
|
||||
public typealias ErrorType = ErrorProtocol
|
||||
|
||||
extension ErrorProtocol {
|
||||
public var _domain: String {
|
||||
return String(reflecting: self.dynamicType)
|
||||
}
|
||||
|
||||
public var _userInfo: AnyObject? {
|
||||
#if _runtime(_ObjC)
|
||||
return _stdlib_getErrorDefaultUserInfo(self)
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,34 @@ extern "C" NSInteger swift_stdlib_getErrorCode(const OpaqueValue *error,
|
||||
const Metadata *T,
|
||||
const WitnessTable *ErrorProtocol);
|
||||
|
||||
//@_silgen_name("swift_stdlib_getErrorUserInfoNSDictionary")
|
||||
//public func _stdlib_getErrorUserInfoNSDictionary<T : ErrorProtocol>(_ x: UnsafePointer<T>) -> AnyObject
|
||||
SWIFT_CC(swift)
|
||||
extern "C" NSDictionary *swift_stdlib_getErrorUserInfoNSDictionary(
|
||||
const OpaqueValue *error,
|
||||
const Metadata *T,
|
||||
const WitnessTable *ErrorProtocol);
|
||||
|
||||
//@_silgen_name("swift_stdlib_getErrorDefaultUserInfo")
|
||||
//public func _stdlib_getErrorDefaultUserInfo<T : ErrorProtocol>(_ x: UnsafePointer<T>) -> AnyObject
|
||||
SWIFT_CC(swift) SWIFT_RT_ENTRY_VISIBILITY
|
||||
extern "C" NSDictionary *swift_stdlib_getErrorDefaultUserInfo(
|
||||
const OpaqueValue *error,
|
||||
const Metadata *T,
|
||||
const WitnessTable *ErrorProtocol) {
|
||||
typedef SWIFT_CC(swift)
|
||||
NSDictionary *GetDefaultFn(const OpaqueValue *error,
|
||||
const Metadata *T,
|
||||
const WitnessTable *ErrorProtocol);
|
||||
|
||||
auto foundationGetDefaultUserInfo = SWIFT_LAZY_CONSTANT(
|
||||
reinterpret_cast<GetDefaultFn*>
|
||||
(dlsym(RTLD_DEFAULT, "swift_Foundation_getErrorDefaultUserInfo")));
|
||||
if (!foundationGetDefaultUserInfo) { return nullptr; }
|
||||
|
||||
return foundationGetDefaultUserInfo(error, T, ErrorProtocol);
|
||||
}
|
||||
|
||||
/// Take an ErrorProtocol box and turn it into a valid NSError instance.
|
||||
SWIFT_CC(swift)
|
||||
static id _swift_bridgeErrorProtocolToNSError_(SwiftError *errorObject) {
|
||||
@@ -302,7 +330,8 @@ static id _swift_bridgeErrorProtocolToNSError_(SwiftError *errorObject) {
|
||||
|
||||
NSString *domain = swift_stdlib_getErrorDomainNSString(value, type, witness);
|
||||
NSInteger code = swift_stdlib_getErrorCode(value, type, witness);
|
||||
// TODO: user info?
|
||||
NSDictionary *userInfo =
|
||||
swift_stdlib_getErrorUserInfoNSDictionary(value, type, witness);
|
||||
|
||||
// The error code shouldn't change, so we can store it blindly, even if
|
||||
// somebody beat us to it. The store can be relaxed, since we'll do a
|
||||
@@ -310,13 +339,21 @@ static id _swift_bridgeErrorProtocolToNSError_(SwiftError *errorObject) {
|
||||
// NSError.
|
||||
errorObject->code.store(code, std::memory_order_relaxed);
|
||||
|
||||
// However, we need to cmpxchg in the domain; if somebody beat us to it,
|
||||
// However, we need to cmpxchg the userInfo; if somebody beat us to it,
|
||||
// we need to release.
|
||||
CFDictionaryRef expectedUserInfo = nullptr;
|
||||
if (!errorObject->userInfo.compare_exchange_strong(expectedUserInfo,
|
||||
(CFDictionaryRef)userInfo,
|
||||
std::memory_order_acq_rel))
|
||||
objc_release(userInfo);
|
||||
|
||||
// We also need to cmpxchg in the domain; if somebody beat us to it,
|
||||
// we need to release.
|
||||
//
|
||||
// Storing the domain must be the LAST THING we do, since it's
|
||||
// the signal that the NSError has been initialized.
|
||||
CFStringRef expected = nullptr;
|
||||
if (!errorObject->domain.compare_exchange_strong(expected,
|
||||
CFStringRef expectedDomain = nullptr;
|
||||
if (!errorObject->domain.compare_exchange_strong(expectedDomain,
|
||||
(CFStringRef)domain,
|
||||
std::memory_order_acq_rel))
|
||||
objc_release(domain);
|
||||
|
||||
@@ -314,4 +314,143 @@ ErrorProtocolBridgingTests.test("Thrown NSError identity is preserved") {
|
||||
}
|
||||
}
|
||||
|
||||
// Check errors customized via protocol.
|
||||
struct MyCustomizedError : ErrorProtocol {
|
||||
let domain: String
|
||||
let code: Int
|
||||
}
|
||||
|
||||
extension MyCustomizedError : LocalizedError {
|
||||
var errorDescription: String? {
|
||||
return NSLocalizedString("something went horribly wrong", comment: "")
|
||||
}
|
||||
|
||||
var failureReason: String? {
|
||||
return NSLocalizedString("because someone wrote 'throw'", comment: "")
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
return NSLocalizedString("delete the 'throw'", comment: "")
|
||||
}
|
||||
|
||||
var helpAnchor: String? {
|
||||
return NSLocalizedString("there is no help when writing tests", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
extension MyCustomizedError : CustomNSError {
|
||||
var errorDomain: String {
|
||||
return domain
|
||||
}
|
||||
|
||||
/// The error code within the given domain.
|
||||
var errorCode: Int {
|
||||
return code
|
||||
}
|
||||
|
||||
/// The user-info dictionary.
|
||||
var errorUserInfo: [String : AnyObject] {
|
||||
return [ NSURLErrorKey : URL(string: "https://swift.org")! as AnyObject]
|
||||
}
|
||||
}
|
||||
|
||||
extension MyCustomizedError : RecoverableError {
|
||||
var recoveryOptions: [String] {
|
||||
return ["Delete 'throw'", "Disable the test" ]
|
||||
}
|
||||
|
||||
func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool {
|
||||
return recoveryOptionIndex == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Fake definition of the informal protocol
|
||||
/// "NSErrorRecoveryAttempting" that we use to poke at the object
|
||||
/// produced for a RecoverableError.
|
||||
@objc protocol FakeNSErrorRecoveryAttempting {
|
||||
@objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
|
||||
func attemptRecovery(fromError nsError: ErrorProtocol,
|
||||
optionIndex recoveryOptionIndex: Int,
|
||||
delegate: AnyObject?,
|
||||
didRecoverSelector: Selector,
|
||||
contextInfo: UnsafeMutablePointer<Void>?)
|
||||
|
||||
@objc(attemptRecoveryFromError:optionIndex:)
|
||||
func attemptRecovery(fromError nsError: ErrorProtocol,
|
||||
optionIndex recoveryOptionIndex: Int) -> Bool
|
||||
}
|
||||
|
||||
class RecoveryDelegate {
|
||||
let expectedSuccess: Bool
|
||||
let expectedContextInfo: UnsafeMutablePointer<Void>?
|
||||
var called = false
|
||||
|
||||
init(expectedSuccess: Bool,
|
||||
expectedContextInfo: UnsafeMutablePointer<Void>?) {
|
||||
self.expectedSuccess = expectedSuccess
|
||||
self.expectedContextInfo = expectedContextInfo
|
||||
}
|
||||
|
||||
@objc func recover(success: Bool, contextInfo: UnsafeMutablePointer<Void>?) {
|
||||
expectEqual(expectedSuccess, success)
|
||||
expectEqual(expectedContextInfo, contextInfo)
|
||||
called = true
|
||||
}
|
||||
}
|
||||
|
||||
ErrorProtocolBridgingTests.test("Customizing NSError via protocols") {
|
||||
let error = MyCustomizedError(domain: "custom", code: 12345)
|
||||
let nsError = error as NSError
|
||||
|
||||
// CustomNSError
|
||||
expectEqual("custom", nsError.domain)
|
||||
expectEqual(12345, nsError.code)
|
||||
expectOptionalEqual(URL(string: "https://swift.org")!,
|
||||
nsError.userInfo[NSURLErrorKey] as? URL)
|
||||
|
||||
// LocalizedError
|
||||
expectOptionalEqual("something went horribly wrong",
|
||||
nsError.userInfo[NSLocalizedDescriptionKey] as? String)
|
||||
expectOptionalEqual("because someone wrote 'throw'",
|
||||
nsError.userInfo[NSLocalizedFailureReasonErrorKey] as? String)
|
||||
expectOptionalEqual("delete the 'throw'",
|
||||
nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String)
|
||||
expectOptionalEqual("there is no help when writing tests",
|
||||
nsError.userInfo[NSHelpAnchorErrorKey] as? String)
|
||||
|
||||
// RecoverableError
|
||||
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
|
||||
nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String])
|
||||
|
||||
// Directly recover.
|
||||
let attempter = nsError.userInfo[NSRecoveryAttempterErrorKey]!
|
||||
expectOptionalEqual(attempter.attemptRecovery(fromError: nsError,
|
||||
optionIndex: 0),
|
||||
true)
|
||||
expectOptionalEqual(attempter.attemptRecovery(fromError: nsError,
|
||||
optionIndex: 1),
|
||||
false)
|
||||
|
||||
// Recover through delegate.
|
||||
let rd1 = RecoveryDelegate(expectedSuccess: true, expectedContextInfo: nil)
|
||||
expectEqual(false, rd1.called)
|
||||
attempter.attemptRecovery(
|
||||
fromError: nsError,
|
||||
optionIndex: 0,
|
||||
delegate: rd1,
|
||||
didRecoverSelector: #selector(RecoveryDelegate.recover(success:contextInfo:)),
|
||||
contextInfo: nil)
|
||||
expectEqual(true, rd1.called)
|
||||
|
||||
let rd2 = RecoveryDelegate(expectedSuccess: false, expectedContextInfo: nil)
|
||||
expectEqual(false, rd2.called)
|
||||
attempter.attemptRecovery(
|
||||
fromError: nsError,
|
||||
optionIndex: 1,
|
||||
delegate: rd2,
|
||||
didRecoverSelector: #selector(RecoveryDelegate.recover(success:contextInfo:)),
|
||||
contextInfo: nil)
|
||||
expectEqual(true, rd2.called)
|
||||
}
|
||||
|
||||
runAllTests()
|
||||
|
||||
Reference in New Issue
Block a user