From 5d1d7932a9a2cc2c529d530a54b4a62cc3fde1e8 Mon Sep 17 00:00:00 2001 From: hank121314 Date: Tue, 13 Dec 2022 01:17:27 +0800 Subject: [PATCH] Validate the default key name and emit with Xcode runtime warning (#126) Co-authored-by: Sindre Sorhus --- Sources/Defaults/Defaults.swift | 35 +++++++++---- Sources/Defaults/Utilities.swift | 67 ++++++++++++++++++++++++- Tests/DefaultsTests/DefaultsTests.swift | 11 +++- 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index e6f807f..09b94b8 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -49,7 +49,13 @@ extension Defaults { public let name: String public let suite: UserDefaults + @_alwaysEmitIntoClient 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.suite = suite } @@ -88,7 +94,8 @@ extension Defaults { - `UserDefaults.object(forKey: string)` returns `nil` - A `bridge` cannot deserialize `Value` from `UserDefaults` */ - private let defaultValueGetter: () -> Value + @usableFromInline + internal let defaultValueGetter: () -> Value 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. */ + @_alwaysEmitIntoClient public init( _ key: String, 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. */ + @_alwaysEmitIntoClient public init( _ key: String, suite: UserDefaults = .standard, @@ -144,18 +153,22 @@ extension Defaults { super.init(name: key, suite: suite) } + } +} - /** - Create a key with an optional value. +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. - - Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`). - */ - public convenience init( - _ key: String, - suite: UserDefaults = .standard - ) where Value == T? { - self.init(key, default: nil, suite: suite) - } + - Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`). + */ + @_transparent + public convenience init( + _ key: String, + suite: UserDefaults = .standard + ) where Value == T? { + self.init(key, default: nil, suite: suite) } } diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index 6d6c86d..ecdf4ea 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -1,5 +1,9 @@ import Foundation - +#if DEBUG +#if canImport(OSLog) +import OSLog +#endif +#endif extension Decodable { 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 { /** @@ -200,7 +211,8 @@ extension Defaults.Serializable { set(Value.toSerialize(value), forKey: key) ``` */ - static func toSerializable(_ value: T) -> Any? { + @usableFromInline + internal static func toSerializable(_ value: T) -> Any? { if T.isNativelySupportedType { return value } @@ -217,3 +229,54 @@ extension Defaults.Serializable { 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.. 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 +} diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index 49c4d41..3dbfc99 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -1,7 +1,7 @@ import Foundation import Combine import XCTest -import Defaults +@testable import Defaults let fixtureURL = URL(string: "https://sindresorhus.com")! let fixtureFileURL = URL(string: "file://~/icon.png")! @@ -37,6 +37,15 @@ final class DefaultsTests: XCTestCase { XCTAssertTrue(Defaults[key]) } + func testValidKeyName() { + let validKey = Defaults.Key("test", default: false) + let containsDotKey = Defaults.Key("test.a", default: false) + let startsWithAtKey = Defaults.Key("@test", default: false) + XCTAssertTrue(Defaults.isValidKeyPath(name: validKey.name)) + XCTAssertFalse(Defaults.isValidKeyPath(name: containsDotKey.name)) + XCTAssertFalse(Defaults.isValidKeyPath(name: startsWithAtKey.name)) + } + func testOptionalKey() { let key = Defaults.Key("independentOptionalKey") let url = Defaults.Key("independentOptionalURLKey")