Skip to content

Commit d53a2f6

Browse files
committed
[SE-0112] Use NSError's user-info value providers to lazily populate NSError
1 parent 1a02de6 commit d53a2f6

File tree

2 files changed

+177
-37
lines changed

2 files changed

+177
-37
lines changed

stdlib/public/SDK/Foundation/NSError.swift

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,13 @@ internal func NS_Swift_performErrorRecoverySelector(
4747
/// NSErrorRecoveryAttempting, which is used by NSError when it
4848
/// attempts recovery from an error.
4949
class _NSErrorRecoveryAttempter {
50-
// FIXME: If we could meaningfully cast the nsError back to RecoverableError,
51-
// we wouldn't need to capture this and could use the user-info
52-
// domain providers even for recoverable errors.
53-
let error: RecoverableError
54-
55-
init(error: RecoverableError) {
56-
self.error = error
57-
}
58-
5950
@objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
6051
func attemptRecovery(fromError nsError: NSError,
6152
optionIndex recoveryOptionIndex: Int,
6253
delegate: AnyObject?,
6354
didRecoverSelector: Selector,
6455
contextInfo: UnsafeMutablePointer<Void>?) {
56+
let error = nsError as Error as! RecoverableError
6557
error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in
6658
NS_Swift_performErrorRecoverySelector(
6759
delegate: delegate,
@@ -74,6 +66,7 @@ class _NSErrorRecoveryAttempter {
7466
@objc(attemptRecoveryFromError:optionIndex:)
7567
func attemptRecovery(fromError nsError: NSError,
7668
optionIndex recoveryOptionIndex: Int) -> Bool {
69+
let error = nsError as Error as! RecoverableError
7770
return error.attemptRecovery(optionIndex: recoveryOptionIndex)
7871
}
7972
}
@@ -147,11 +140,54 @@ public extension Error {
147140
@_silgen_name("swift_Foundation_getErrorDefaultUserInfo")
148141
public func _swift_Foundation_getErrorDefaultUserInfo(_ error: Error)
149142
-> AnyObject? {
143+
let hasUserInfoValueProvider: Bool
144+
150145
// If the OS supports user info value providers, use those
151146
// to lazily populate the user-info dictionary for this domain.
152147
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
153-
// FIXME: This is not implementable until we can recover the
154-
// original error from an NSError.
148+
// Note: the Cocoa error domain specifically excluded from
149+
// user-info value providers.
150+
let domain = error._domain
151+
if domain != NSCocoaErrorDomain {
152+
if NSError.userInfoValueProvider(forDomain: domain) == nil {
153+
NSError.setUserInfoValueProvider(forDomain: domain) { (nsError, key) in
154+
let error = nsError as Error
155+
156+
switch key {
157+
case NSLocalizedDescriptionKey:
158+
return (error as? LocalizedError)?.errorDescription
159+
160+
case NSLocalizedFailureReasonErrorKey:
161+
return (error as? LocalizedError)?.failureReason
162+
163+
case NSLocalizedRecoverySuggestionErrorKey:
164+
return (error as? LocalizedError)?.recoverySuggestion
165+
166+
case NSHelpAnchorErrorKey:
167+
return (error as? LocalizedError)?.helpAnchor
168+
169+
case NSLocalizedRecoveryOptionsErrorKey:
170+
return (error as? RecoverableError)?.recoveryOptions
171+
172+
case NSRecoveryAttempterErrorKey:
173+
if error is RecoverableError {
174+
return _NSErrorRecoveryAttempter()
175+
}
176+
return nil
177+
178+
default:
179+
return nil
180+
}
181+
}
182+
}
183+
assert(NSError.userInfoValueProvider(forDomain: domain) != nil)
184+
185+
hasUserInfoValueProvider = true
186+
} else {
187+
hasUserInfoValueProvider = false
188+
}
189+
} else {
190+
hasUserInfoValueProvider = false
155191
}
156192

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

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

185-
if let recoverableError = error as? RecoverableError {
224+
// Handle recoverable errors. If we registered a user-info value
225+
// provider, these will computed lazily.
226+
if !hasUserInfoValueProvider,
227+
let recoverableError = error as? RecoverableError {
186228
result[NSLocalizedRecoveryOptionsErrorKey] =
187229
recoverableError.recoveryOptions as AnyObject
188-
result[NSRecoveryAttempterErrorKey] =
189-
_NSErrorRecoveryAttempter(error: recoverableError)
230+
result[NSRecoveryAttempterErrorKey] = _NSErrorRecoveryAttempter()
190231
}
191232

192233
return result as AnyObject

test/1_stdlib/ErrorBridged.swift

Lines changed: 121 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,43 @@ extension MyCustomizedError : RecoverableError {
391391
}
392392
}
393393

394+
/// An error type that provides localization and recovery, but doesn't
395+
/// customize NSError directly.
396+
enum MySwiftCustomizedError : Error {
397+
case failed
398+
static var errorDescriptionCount = 0
399+
}
400+
401+
extension MySwiftCustomizedError : LocalizedError {
402+
var errorDescription: String? {
403+
MySwiftCustomizedError.errorDescriptionCount =
404+
MySwiftCustomizedError.errorDescriptionCount + 1
405+
return NSLocalizedString("something went horribly wrong", comment: "")
406+
}
407+
408+
var failureReason: String? {
409+
return NSLocalizedString("because someone wrote 'throw'", comment: "")
410+
}
411+
412+
var recoverySuggestion: String? {
413+
return NSLocalizedString("delete the 'throw'", comment: "")
414+
}
415+
416+
var helpAnchor: String? {
417+
return NSLocalizedString("there is no help when writing tests", comment: "")
418+
}
419+
}
420+
421+
extension MySwiftCustomizedError : RecoverableError {
422+
var recoveryOptions: [String] {
423+
return ["Delete 'throw'", "Disable the test" ]
424+
}
425+
426+
func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool {
427+
return recoveryOptionIndex == 0
428+
}
429+
}
430+
394431
/// Fake definition of the informal protocol
395432
/// "NSErrorRecoveryAttempting" that we use to poke at the object
396433
/// produced for a RecoverableError.
@@ -425,34 +462,48 @@ class RecoveryDelegate {
425462
}
426463
}
427464

428-
ErrorBridgingTests.test("Customizing NSError via protocols") {
429-
let error = MyCustomizedError(code: 12345)
430-
let nsError = error as NSError
431-
432-
// CustomNSError
433-
expectEqual("custom", nsError.domain)
434-
expectEqual(12345, nsError.code)
435-
expectOptionalEqual(URL(string: "https://swift.org")!,
436-
nsError.userInfo[NSURLErrorKey] as? URL)
437-
465+
/// Helper for testing a customized error.
466+
func testCustomizedError(error: Error, nsError: NSError) {
438467
// LocalizedError
439-
expectOptionalEqual("something went horribly wrong",
440-
nsError.userInfo[NSLocalizedDescriptionKey] as? String)
441-
expectOptionalEqual("because someone wrote 'throw'",
442-
nsError.userInfo[NSLocalizedFailureReasonErrorKey] as? String)
443-
expectOptionalEqual("delete the 'throw'",
444-
nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String)
445-
expectOptionalEqual("there is no help when writing tests",
446-
nsError.userInfo[NSHelpAnchorErrorKey] as? String)
447-
expectEqual(nsError.localizedDescription, "something went horribly wrong")
448-
expectEqual(error.localizedDescription, "something went horribly wrong")
468+
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
469+
expectEmpty(nsError.userInfo[NSLocalizedDescriptionKey])
470+
expectEmpty(nsError.userInfo[NSLocalizedFailureReasonErrorKey])
471+
expectEmpty(nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey])
472+
expectEmpty(nsError.userInfo[NSHelpAnchorErrorKey])
473+
} else {
474+
expectOptionalEqual("something went horribly wrong",
475+
nsError.userInfo[NSLocalizedDescriptionKey] as? String)
476+
expectOptionalEqual("because someone wrote 'throw'",
477+
nsError.userInfo[NSLocalizedFailureReasonErrorKey] as? String)
478+
expectOptionalEqual("delete the 'throw'",
479+
nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String)
480+
expectOptionalEqual("there is no help when writing tests",
481+
nsError.userInfo[NSHelpAnchorErrorKey] as? String)
482+
}
483+
expectEqual("something went horribly wrong", error.localizedDescription)
484+
expectEqual("something went horribly wrong", nsError.localizedDescription)
485+
expectEqual("because someone wrote 'throw'", nsError.localizedFailureReason)
486+
expectEqual("delete the 'throw'", nsError.localizedRecoverySuggestion)
487+
expectEqual("there is no help when writing tests", nsError.helpAnchor)
449488

450489
// RecoverableError
490+
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
491+
expectEmpty(nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey])
492+
} else {
493+
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
494+
nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String])
495+
}
451496
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
452-
nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String])
497+
nsError.localizedRecoveryOptions)
453498

454499
// Directly recover.
455-
let attempter = nsError.userInfo[NSRecoveryAttempterErrorKey]!
500+
let attempter: AnyObject
501+
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
502+
expectEmpty(nsError.userInfo[NSRecoveryAttempterErrorKey])
503+
attempter = nsError.recoveryAttempter!
504+
} else {
505+
attempter = nsError.userInfo[NSRecoveryAttempterErrorKey]!
506+
}
456507
expectOptionalEqual(attempter.attemptRecovery(fromError: nsError,
457508
optionIndex: 0),
458509
true)
@@ -482,4 +533,52 @@ ErrorBridgingTests.test("Customizing NSError via protocols") {
482533
expectEqual(true, rd2.called)
483534
}
484535

536+
ErrorBridgingTests.test("Customizing NSError via protocols") {
537+
let error = MyCustomizedError(code: 12345)
538+
let nsError = error as NSError
539+
540+
// CustomNSError
541+
expectEqual("custom", nsError.domain)
542+
expectEqual(12345, nsError.code)
543+
expectOptionalEqual(URL(string: "https://swift.org")!,
544+
nsError.userInfo[NSURLErrorKey] as? URL)
545+
546+
testCustomizedError(error: error, nsError: nsError)
547+
}
548+
549+
ErrorBridgingTests.test("Customizing localization/recovery via protocols") {
550+
let error = MySwiftCustomizedError.failed
551+
let nsError = error as NSError
552+
testCustomizedError(error: error, nsError: nsError)
553+
}
554+
555+
ErrorBridgingTests.test("Customizing localization/recovery laziness") {
556+
let countBefore = MySwiftCustomizedError.errorDescriptionCount
557+
let error = MySwiftCustomizedError.failed
558+
let nsError = error as NSError
559+
560+
// RecoverableError
561+
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
562+
expectEmpty(nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey])
563+
} else {
564+
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
565+
nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String])
566+
}
567+
expectOptionalEqual(["Delete 'throw'", "Disable the test" ],
568+
nsError.localizedRecoveryOptions)
569+
570+
// None of the operations above should affect the count
571+
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
572+
expectEqual(countBefore, MySwiftCustomizedError.errorDescriptionCount)
573+
}
574+
575+
// This one does affect the count.
576+
expectEqual("something went horribly wrong", error.localizedDescription)
577+
578+
// Check that we did get a call to errorDescription.
579+
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
580+
expectEqual(countBefore+1, MySwiftCustomizedError.errorDescriptionCount)
581+
}
582+
}
583+
485584
runAllTests()

0 commit comments

Comments
 (0)