Skip to content
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

[SR-14975] LocalizedError properties get mangled by Objective-C bridge #3364

Open
CharlesJS opened this issue Jul 25, 2021 · 1 comment
Open

Comments

@CharlesJS
Copy link

Previous ID SR-14975
Radar rdar://problem/81115919
Original Reporter @CharlesJS
Type Bug
Environment

macOS 21A5284e

Xcode Version 13.0 beta 3 (13A5192i)

Swift version 5.5 (swiftlang-1300.0.24.13 clang-1300.0.25.10)

Additional Detail from JIRA
Votes 0
Component/s Foundation
Labels Bug
Assignee None
Priority Medium

md5: f34c7dac79752ed46ed183110bcd487a

Issue Description:

So, let's say we have an enum that conforms to `LocalizedError`, which contains logic to bridge nicely to `NSError` so that Cocoa UIs will present our errors nicely:

import Foundation

enum Plague: LocalizedError {
    case fires
    case floods
    case frogs
    case flies
    
    var failureReason: String? {
        switch self {
        case .fires:
            return "We didn't start the fire! But this error did"
        case .floods:
            return "Ya got trouble, right here in River City"
        case .frogs:
            return "Ribbit Ribbit"
        case .flies:
            return "Shoo fly, don't bother me"
        }
    }
}

let err = Plague.frogs

print(
    "Before encoding/decoding: localizedDescription is '\((err as NSError).localizedDescription)', ",
    "userInfo is \((err as NSError).userInfo)"
)

Running that, we get:

Before encoding/decoding: localizedDescription is 'The operation couldn’t be completed. Ribbit Ribbit', userInfo is [:]

The `userInfo` dictionary is empty, which is odd, but the `localizedDescription` does get displayed correctly after we bridge to `NSError`. So far so good.

But what if it goes through an `NSKeyedArchiver` (for example, if it gets sent over an XPC connection)?

let encoded = try! NSKeyedArchiver.archivedData(withRootObject: err, requiringSecureCoding: true)

let decoded = try! NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: encoded)!

print("After: localizedDescription is '\(decoded.localizedDescription)', userInfo is \(decoded.userInfo)")

Suddenly, the `failureReason` string is lost:

After: localizedDescription is 'The operation couldn’t be completed. (main.Plague error 2.)', userInfo is [:]

However, since `Error` contains a `_userInfo` property, we can add an implemention for that ourselves:

var _userInfo: AnyObject? { [NSLocalizedFailureReasonErrorKey : self.failureReason!] as NSDictionary }

And that fixes it:

After: localizedDescription is 'The operation couldn’t be completed. Ribbit Ribbit', userInfo is ["NSLocalizedFailureReason": Ribbit Ribbit]

However: this only seems to work on error classes defined in the current module. If I try to add `_userInfo` to something defined in another module via an extension, it doesn't work:

extension POSIXError {
    var _userInfo: AnyObject? { [NSLocalizedFailureReasonErrorKey : "Hello"] as NSDictionary }
}

let err = POSIXError(.EINVAL)

print(
    "Before encoding/decoding: localizedDescription is '\((err as NSError).localizedDescription)', ",
    "userInfo is \((err as NSError).userInfo)"
)

let encoded = try! NSKeyedArchiver.archivedData(withRootObject: err, requiringSecureCoding: true)
let decoded = try! NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: encoded)!

print("After: localizedDescription is '\(decoded.localizedDescription)', userInfo is \(decoded.userInfo)")

yields:

Before encoding/decoding: localizedDescription is 'The operation couldn’t be completed. Invalid argument',  userInfo is [:]
After: localizedDescription is 'The operation couldn’t be completed. Invalid argument', userInfo is [:]

This is not exclusive to Foundation-defined things like `POSIXError`; I've tried it with errors I've defined myself in other modules, and the behavior is the same. I can put a log in the `_userInfo` accessor, and it never gets called.

It seems to me that the expected behavior should be:

  1. `LocalizedString` includes a default implementation for `_userInfo` that provides the various strings corresponding to the properties it supports.

  2. We have logic in place to make sure `_userInfo` always gets propagated across when bridging to `NSError`, or at least when we're running it through an encoder.

Otherwise, the `userInfo` field is always unpopulated for bridged Swift errors, and in the end, all I have is an arbitrary integer and I don't know whether I need to get an extinguisher, an umbrella, some bug spray, or... whatever you do for frogs (a cat maybe?).

@typesanitizer
Copy link

@swift-ci create

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
@shahmishal shahmishal transferred this issue from apple/swift May 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants