Validate the default key name and emit with Xcode runtime warning (#126)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
hank121314 2022-12-13 01:17:27 +08:00 committed by GitHub
parent 5cf1178d34
commit 5d1d7932a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 14 deletions

View File

@ -49,7 +49,13 @@ extension Defaults {
public let name: String public let name: String
public let suite: UserDefaults public let suite: UserDefaults
@_alwaysEmitIntoClient
fileprivate init(name: String, suite: UserDefaults) { fileprivate init(name: String, suite: UserDefaults) {
runtimeWarn(
isValidKeyPath(name: name),
"The key name must be ASCII, not start with @, and cannot contain a dot (.)."
)
self.name = name self.name = name
self.suite = suite self.suite = suite
} }
@ -88,7 +94,8 @@ extension Defaults {
- `UserDefaults.object(forKey: string)` returns `nil` - `UserDefaults.object(forKey: string)` returns `nil`
- A `bridge` cannot deserialize `Value` from `UserDefaults` - A `bridge` cannot deserialize `Value` from `UserDefaults`
*/ */
private let defaultValueGetter: () -> Value @usableFromInline
internal let defaultValueGetter: () -> Value
public var defaultValue: Value { defaultValueGetter() } public var defaultValue: Value { defaultValueGetter() }
@ -99,6 +106,7 @@ extension Defaults {
The `default` parameter should not be used if the `Value` type is an optional. The `default` parameter should not be used if the `Value` type is an optional.
*/ */
@_alwaysEmitIntoClient
public init( public init(
_ key: String, _ key: String,
default defaultValue: Value, default defaultValue: Value,
@ -135,6 +143,7 @@ extension Defaults {
- Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings. - Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings.
*/ */
@_alwaysEmitIntoClient
public init( public init(
_ key: String, _ key: String,
suite: UserDefaults = .standard, suite: UserDefaults = .standard,
@ -144,19 +153,23 @@ extension Defaults {
super.init(name: key, suite: suite) super.init(name: key, suite: suite)
} }
}
}
extension Defaults.Key {
// We cannot declare this convenience initializer in class directly because of "@_transparent' attribute is not supported on declarations within classes".
/** /**
Create a key with an optional value. Create a key with an optional value.
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`). - Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
*/ */
@_transparent
public convenience init<T>( public convenience init<T>(
_ key: String, _ key: String,
suite: UserDefaults = .standard suite: UserDefaults = .standard
) where Value == T? { ) where Value == T? {
self.init(key, default: nil, suite: suite) self.init(key, default: nil, suite: suite)
} }
}
} }
extension Defaults { extension Defaults {

View File

@ -1,5 +1,9 @@
import Foundation import Foundation
#if DEBUG
#if canImport(OSLog)
import OSLog
#endif
#endif
extension Decodable { extension Decodable {
init?(jsonData: Data) { init?(jsonData: Data) {
@ -160,6 +164,13 @@ extension Collection {
} }
} }
extension Defaults {
@usableFromInline
internal static func isValidKeyPath(name: String) -> Bool {
// The key must be ASCII, not start with @, and cannot contain a dot.
return !name.starts(with: "@") && name.allSatisfy { $0 != "." && $0.isASCII }
}
}
extension Defaults.Serializable { extension Defaults.Serializable {
/** /**
@ -200,7 +211,8 @@ extension Defaults.Serializable {
set(Value.toSerialize(value), forKey: key) set(Value.toSerialize(value), forKey: key)
``` ```
*/ */
static func toSerializable<T: Defaults.Serializable>(_ value: T) -> Any? { @usableFromInline
internal static func toSerializable<T: Defaults.Serializable>(_ value: T) -> Any? {
if T.isNativelySupportedType { if T.isNativelySupportedType {
return value return value
} }
@ -217,3 +229,54 @@ extension Defaults.Serializable {
return toSerializable(next) return toSerializable(next)
} }
} }
#if DEBUG
/**
Get SwiftUI dynamic shared object.
Reference: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dyld.3.html
*/
@usableFromInline
internal let dynamicSharedObject: UnsafeMutableRawPointer = {
let imageCount = _dyld_image_count()
for imageIndex in 0..<imageCount {
guard
let name = _dyld_get_image_name(imageIndex),
// Use `/SwiftUI` instead of `SwiftUI` to prevent any library named `XXSwiftUI`.
String(cString: name).hasSuffix("/SwiftUI"),
let header = _dyld_get_image_header(imageIndex)
else {
continue
}
return UnsafeMutableRawPointer(mutating: header)
}
return UnsafeMutableRawPointer(mutating: #dsohandle)
}()
#endif
@_transparent
@usableFromInline
internal func runtimeWarn(
_ condition: @autoclosure() -> Bool, _ message: @autoclosure () -> String
) {
#if DEBUG
#if canImport(OSLog)
let message = message()
let condition = condition()
if !condition {
os_log(
.fault,
// A token that identifies the containing executable or dylib image.
dso: dynamicSharedObject,
log: OSLog(subsystem: "com.apple.runtime-issues", category: "Defaults"),
"%@",
message
)
}
#else
assert(condition, message)
#endif
#endif
}

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
import Combine import Combine
import XCTest import XCTest
import Defaults @testable import Defaults
let fixtureURL = URL(string: "https://sindresorhus.com")! let fixtureURL = URL(string: "https://sindresorhus.com")!
let fixtureFileURL = URL(string: "file://~/icon.png")! let fixtureFileURL = URL(string: "file://~/icon.png")!
@ -37,6 +37,15 @@ final class DefaultsTests: XCTestCase {
XCTAssertTrue(Defaults[key]) XCTAssertTrue(Defaults[key])
} }
func testValidKeyName() {
let validKey = Defaults.Key<Bool>("test", default: false)
let containsDotKey = Defaults.Key<Bool>("test.a", default: false)
let startsWithAtKey = Defaults.Key<Bool>("@test", default: false)
XCTAssertTrue(Defaults.isValidKeyPath(name: validKey.name))
XCTAssertFalse(Defaults.isValidKeyPath(name: containsDotKey.name))
XCTAssertFalse(Defaults.isValidKeyPath(name: startsWithAtKey.name))
}
func testOptionalKey() { func testOptionalKey() {
let key = Defaults.Key<Bool?>("independentOptionalKey") let key = Defaults.Key<Bool?>("independentOptionalKey")
let url = Defaults.Key<URL?>("independentOptionalURLKey") let url = Defaults.Key<URL?>("independentOptionalURLKey")