Skip to content

Commit

Permalink
[SE-0112] Use NSError's user-info value providers to lazily populate …
Browse files Browse the repository at this point in the history
…NSError
  • Loading branch information
DougGregor committed Jul 12, 2016
1 parent 1a02de6 commit d53a2f6
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 37 deletions.
71 changes: 56 additions & 15 deletions stdlib/public/SDK/Foundation/NSError.swift
Expand Up @@ -47,21 +47,13 @@ internal func NS_Swift_performErrorRecoverySelector(
/// 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>?) {
let error = nsError as Error as! RecoverableError
error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in
NS_Swift_performErrorRecoverySelector(
delegate: delegate,
Expand All @@ -74,6 +66,7 @@ class _NSErrorRecoveryAttempter {
@objc(attemptRecoveryFromError:optionIndex:)
func attemptRecovery(fromError nsError: NSError,
optionIndex recoveryOptionIndex: Int) -> Bool {
let error = nsError as Error as! RecoverableError
return error.attemptRecovery(optionIndex: recoveryOptionIndex)
}
}
Expand Down Expand Up @@ -147,11 +140,54 @@ public extension Error {
@_silgen_name("swift_Foundation_getErrorDefaultUserInfo")
public func _swift_Foundation_getErrorDefaultUserInfo(_ error: Error)
-> AnyObject? {
let hasUserInfoValueProvider: Bool

// If the OS supports 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.
// Note: the Cocoa error domain specifically excluded from
// user-info value providers.
let domain = error._domain
if domain != NSCocoaErrorDomain {
if NSError.userInfoValueProvider(forDomain: domain) == nil {
NSError.setUserInfoValueProvider(forDomain: domain) { (nsError, key) in
let error = nsError as Error

switch key {
case NSLocalizedDescriptionKey:
return (error as? LocalizedError)?.errorDescription

case NSLocalizedFailureReasonErrorKey:
return (error as? LocalizedError)?.failureReason

case NSLocalizedRecoverySuggestionErrorKey:
return (error as? LocalizedError)?.recoverySuggestion

case NSHelpAnchorErrorKey:
return (error as? LocalizedError)?.helpAnchor

case NSLocalizedRecoveryOptionsErrorKey:
return (error as? RecoverableError)?.recoveryOptions

case NSRecoveryAttempterErrorKey:
if error is RecoverableError {
return _NSErrorRecoveryAttempter()
}
return nil

default:
return nil
}
}
}
assert(NSError.userInfoValueProvider(forDomain: domain) != nil)

hasUserInfoValueProvider = true
} else {
hasUserInfoValueProvider = false
}
} else {
hasUserInfoValueProvider = false
}

// Populate the user-info dictionary
Expand All @@ -164,7 +200,10 @@ public func _swift_Foundation_getErrorDefaultUserInfo(_ error: Error)
result = [:]
}

if let localizedError = error as? LocalizedError {
// Handle localized errors. If we registered a user-info value
// provider, these will computed lazily.
if !hasUserInfoValueProvider,
let localizedError = error as? LocalizedError {
if let description = localizedError.errorDescription {
result[NSLocalizedDescriptionKey] = description as AnyObject
}
Expand All @@ -182,11 +221,13 @@ public func _swift_Foundation_getErrorDefaultUserInfo(_ error: Error)
}
}

if let recoverableError = error as? RecoverableError {
// Handle recoverable errors. If we registered a user-info value
// provider, these will computed lazily.
if !hasUserInfoValueProvider,
let recoverableError = error as? RecoverableError {
result[NSLocalizedRecoveryOptionsErrorKey] =
recoverableError.recoveryOptions as AnyObject
result[NSRecoveryAttempterErrorKey] =
_NSErrorRecoveryAttempter(error: recoverableError)
result[NSRecoveryAttempterErrorKey] = _NSErrorRecoveryAttempter()
}

return result as AnyObject
Expand Down
143 changes: 121 additions & 22 deletions test/1_stdlib/ErrorBridged.swift
Expand Up @@ -391,6 +391,43 @@ extension MyCustomizedError : RecoverableError {
}
}

/// An error type that provides localization and recovery, but doesn't
/// customize NSError directly.
enum MySwiftCustomizedError : Error {
case failed
static var errorDescriptionCount = 0
}

extension MySwiftCustomizedError : LocalizedError {
var errorDescription: String? {
MySwiftCustomizedError.errorDescriptionCount =
MySwiftCustomizedError.errorDescriptionCount + 1
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 MySwiftCustomizedError : 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.
Expand Down Expand Up @@ -425,34 +462,48 @@ class RecoveryDelegate {
}
}

ErrorBridgingTests.test("Customizing NSError via protocols") {
let error = MyCustomizedError(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)

/// Helper for testing a customized error.
func testCustomizedError(error: Error, nsError: NSError) {
// 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)
expectEqual(nsError.localizedDescription, "something went horribly wrong")
expectEqual(error.localizedDescription, "something went horribly wrong")
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
expectEmpty(nsError.userInfo[NSLocalizedDescriptionKey])
expectEmpty(nsError.userInfo[NSLocalizedFailureReasonErrorKey])
expectEmpty(nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey])
expectEmpty(nsError.userInfo[NSHelpAnchorErrorKey])
} else {
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)
}
expectEqual("something went horribly wrong", error.localizedDescription)
expectEqual("something went horribly wrong", nsError.localizedDescription)
expectEqual("because someone wrote 'throw'", nsError.localizedFailureReason)
expectEqual("delete the 'throw'", nsError.localizedRecoverySuggestion)
expectEqual("there is no help when writing tests", nsError.helpAnchor)

// RecoverableError
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
expectEmpty(nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey])
} else {
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String])
}
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String])
nsError.localizedRecoveryOptions)

// Directly recover.
let attempter = nsError.userInfo[NSRecoveryAttempterErrorKey]!
let attempter: AnyObject
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
expectEmpty(nsError.userInfo[NSRecoveryAttempterErrorKey])
attempter = nsError.recoveryAttempter!
} else {
attempter = nsError.userInfo[NSRecoveryAttempterErrorKey]!
}
expectOptionalEqual(attempter.attemptRecovery(fromError: nsError,
optionIndex: 0),
true)
Expand Down Expand Up @@ -482,4 +533,52 @@ ErrorBridgingTests.test("Customizing NSError via protocols") {
expectEqual(true, rd2.called)
}

ErrorBridgingTests.test("Customizing NSError via protocols") {
let error = MyCustomizedError(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)

testCustomizedError(error: error, nsError: nsError)
}

ErrorBridgingTests.test("Customizing localization/recovery via protocols") {
let error = MySwiftCustomizedError.failed
let nsError = error as NSError
testCustomizedError(error: error, nsError: nsError)
}

ErrorBridgingTests.test("Customizing localization/recovery laziness") {
let countBefore = MySwiftCustomizedError.errorDescriptionCount
let error = MySwiftCustomizedError.failed
let nsError = error as NSError

// RecoverableError
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
expectEmpty(nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey])
} else {
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String])
}
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
nsError.localizedRecoveryOptions)

// None of the operations above should affect the count
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
expectEqual(countBefore, MySwiftCustomizedError.errorDescriptionCount)
}

// This one does affect the count.
expectEqual("something went horribly wrong", error.localizedDescription)

// Check that we did get a call to errorDescription.
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
expectEqual(countBefore+1, MySwiftCustomizedError.errorDescriptionCount)
}
}

runAllTests()

0 comments on commit d53a2f6

Please sign in to comment.