From ea9e9e2665817241c58b7ffa43debd2bfc4bdd41 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Mon, 19 May 2025 14:29:10 -0400 Subject: [PATCH 1/9] (151106260) Forward ObjC messages to _BridgedURL in rare compatibility cases (#1301) --- Sources/FoundationEssentials/URL/URL.swift | 4 +++- .../FoundationEssentials/URL/URL_Bridge.swift | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 948322a4b..a188f5378 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -635,13 +635,15 @@ public struct URL: Equatable, Sendable, Hashable { #if FOUNDATION_FRAMEWORK private static var _type: any _URLProtocol.Type { + if URL.compatibility2 { + return _BridgedURL.self + } return foundation_swift_url_enabled() ? _SwiftURL.self : _BridgedURL.self } #else private static let _type = _SwiftURL.self #endif - #if FOUNDATION_FRAMEWORK internal let _url: any _URLProtocol & AnyObject internal init(_ url: any _URLProtocol & AnyObject) { diff --git a/Sources/FoundationEssentials/URL/URL_Bridge.swift b/Sources/FoundationEssentials/URL/URL_Bridge.swift index 867c06624..8159eeb0c 100644 --- a/Sources/FoundationEssentials/URL/URL_Bridge.swift +++ b/Sources/FoundationEssentials/URL/URL_Bridge.swift @@ -14,17 +14,32 @@ internal import _ForSwiftFoundation internal import CoreFoundation_Private.CFURL +#if canImport(os) +internal import os +#endif + /// `_BridgedURL` wraps an `NSURL` reference. Its methods use the old implementations, which call directly into `NSURL` methods. /// `_BridgedURL` is used when an `NSURL` subclass is bridged to Swift, allowing us to: /// 1) Return the same subclass object when bridging back to ObjC. /// 2) Call methods that are overridden by the `NSURL` subclass like we did before. /// - Note: If the `NSURL` subclass does not override a method, `NSURL` will call into the underlying `_SwiftURL` implementation. -internal final class _BridgedURL: _URLProtocol, @unchecked Sendable { +internal final class _BridgedURL: NSObject, _URLProtocol, @unchecked Sendable { private let _url: NSURL internal init(_ url: NSURL) { self._url = url } + private static let logForwardingErrorOnce: Void = { + #if canImport(os) + URL.logger.error("struct URL no longer stores an NSURL. Clients should not assume the memory address of a URL will contain an NSURL * or CFURLRef and should not send ObjC messages to it directly. Bridge (url as NSURL) instead.") + #endif + }() + + override func forwardingTarget(for aSelector: Selector!) -> Any? { + _ = Self.logForwardingErrorOnce + return _url + } + init?(string: String) { guard !string.isEmpty, let inner = NSURL(string: string) else { return nil } _url = inner @@ -384,11 +399,11 @@ internal final class _BridgedURL: _URLProtocol, @unchecked Sendable { } #endif - var description: String { + override var description: String { return _url.description } - var debugDescription: String { + override var debugDescription: String { return _url.debugDescription } From 6577f3a92bfe14a34d12b84ef2bbb43c91a05b23 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Tue, 20 May 2025 01:58:04 +0200 Subject: [PATCH 2/9] fix `Calendar.RecurrenceRule` (#1284) --- .../Calendar/Calendar_Recurrence.swift | 12 ++++++++++++ .../GregorianCalendarRecurrenceRuleTests.swift | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift index 1a1fac7e9..173558761 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift @@ -93,6 +93,9 @@ extension Calendar { /// value is used as a lower bound for ``nextBaseRecurrenceDate()``. let rangeLowerBound: Date? + /// The start date's nanoseconds component + let startDateNanoseconds: TimeInterval + /// How many occurrences have been found so far var resultsFound = 0 @@ -232,6 +235,8 @@ extension Calendar { } var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start) + startDateNanoseconds = start.timeIntervalSince1970.remainder(dividingBy: 1) + let expansionChangesDay = dayOfYearAction == .expand || dayOfMonthAction == .expand || weekAction == .expand || weekdayAction == .expand let expansionChangesMonth = dayOfYearAction == .expand || monthAction == .expand || weekAction == .expand @@ -422,6 +427,13 @@ extension Calendar { recurrence._limitTimeComponent(.second, dates: &dates, anchor: anchor) } + if startDateNanoseconds > 0 { + // `_dates(startingAfter:)` above returns whole-second dates, + // so we need to restore the nanoseconds value present in the original start date. + for idx in dates.indices { + dates[idx] += startDateNanoseconds + } + } dates = dates.filter { $0 >= self.start } if let limit = recurrence.end.date { diff --git a/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift b/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift index f95cd37cc..112a4c45e 100644 --- a/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift +++ b/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift @@ -803,4 +803,16 @@ final class GregorianCalendarRecurrenceRuleTests: XCTestCase { ] XCTAssertEqual(results, expectedResults) } + + func testDailyRecurrenceRuleWithNonzeroNanosecondComponent() { + let start = Date(timeIntervalSince1970: 1746627600.5) // 2025-05-07T07:20:00.500-07:00 + let rule = Calendar.RecurrenceRule.daily(calendar: gregorian, end: .afterOccurrences(2)) + let results = Array(rule.recurrences(of: start)) + + let expectedResults: [Date] = [ + start, + Date(timeIntervalSince1970: 1746714000.5), // 2025-05-08T07:20:00.500-07:00 + ] + XCTAssertEqual(results, expectedResults) + } } From 0f62460907181a3238ff72c54acfbf84b13b5fac Mon Sep 17 00:00:00 2001 From: Jevon Mao Date: Mon, 19 May 2025 20:15:35 -0700 Subject: [PATCH 3/9] Fix #1176 JSON Decoder and Encoder limit disagreement (#1242) * Add test case to reproduce JSON bug * Increment encoding limit by 1 to match decoding behavior * Move checking condition & add expect fail test * Skip failing test on windows --- .../JSON/JSONScanner.swift | 5 +- .../JSON/JSONWriter.swift | 15 +- .../JSONEncoderTests.swift | 32 +- .../Resources/JSON/fail/fail42.json | 1027 +++++++++++++++++ .../Resources/JSON/pass/pass16.json | 1025 ++++++++++++++++ 5 files changed, 2083 insertions(+), 21 deletions(-) create mode 100644 Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json create mode 100644 Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json diff --git a/Sources/FoundationEssentials/JSON/JSONScanner.swift b/Sources/FoundationEssentials/JSON/JSONScanner.swift index f78c3637b..9f368cb97 100644 --- a/Sources/FoundationEssentials/JSON/JSONScanner.swift +++ b/Sources/FoundationEssentials/JSON/JSONScanner.swift @@ -265,6 +265,7 @@ internal struct JSONScanner { var reader: DocumentReader var depth: Int = 0 var partialMap = JSONPartialMapData() + private static let maximumRecursionDepth = 512 internal struct Options { var assumesTopLevelDictionary = false @@ -412,7 +413,7 @@ internal struct JSONScanner { mutating func scanArray() throws { let firstChar = reader.read() precondition(firstChar == ._openbracket) - guard self.depth < 512 else { + guard self.depth < Self.maximumRecursionDepth else { throw JSONError.tooManyNestedArraysOrDictionaries(location: reader.sourceLocation(atOffset: 1)) } self.depth &+= 1 @@ -470,7 +471,7 @@ internal struct JSONScanner { mutating func scanObject() throws { let firstChar = self.reader.read() precondition(firstChar == ._openbrace) - guard self.depth < 512 else { + guard self.depth < Self.maximumRecursionDepth else { throw JSONError.tooManyNestedArraysOrDictionaries(location: reader.sourceLocation(atOffset: -1)) } try scanObject(withoutBraces: false) diff --git a/Sources/FoundationEssentials/JSON/JSONWriter.swift b/Sources/FoundationEssentials/JSON/JSONWriter.swift index 2ecf8b7b5..b8ff000a7 100644 --- a/Sources/FoundationEssentials/JSON/JSONWriter.swift +++ b/Sources/FoundationEssentials/JSON/JSONWriter.swift @@ -29,6 +29,9 @@ internal struct JSONWriter { } mutating func serializeJSON(_ value: JSONEncoderValue, depth: Int = 0) throws { + guard depth < Self.maximumRecursionDepth else { + throw JSONError.tooManyNestedArraysOrDictionaries() + } switch value { case .string(let str): serializeString(str) @@ -172,10 +175,6 @@ internal struct JSONWriter { } mutating func serializeArray(_ array: [JSONEncoderValue], depth: Int) throws { - guard depth < Self.maximumRecursionDepth else { - throw JSONError.tooManyNestedArraysOrDictionaries() - } - writer(ascii: ._openbracket) if pretty { writer(ascii: ._newline) @@ -204,10 +203,6 @@ internal struct JSONWriter { } mutating func serializePreformattedByteArray(_ bytes: [UInt8], _ lengths: [Int], depth: Int) throws { - guard depth < Self.maximumRecursionDepth else { - throw JSONError.tooManyNestedArraysOrDictionaries() - } - writer(ascii: ._openbracket) if pretty { writer(ascii: ._newline) @@ -242,10 +237,6 @@ internal struct JSONWriter { } mutating func serializeObject(_ dict: [String:JSONEncoderValue], depth: Int) throws { - guard depth < Self.maximumRecursionDepth else { - throw JSONError.tooManyNestedArraysOrDictionaries() - } - writer(ascii: ._openbrace) if pretty { writer(ascii: ._newline) diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 6697df493..3b18e0baa 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -2492,13 +2492,19 @@ extension JSONEncoderTests { prettyPrintEncoder.outputFormatting = .prettyPrinted for encoder in [JSONEncoder(), prettyPrintEncoder] { - let reencodedData = try! encoder.encode(decoded) - let redecodedObjects = try! decoder.decode(T.self, from: reencodedData) - XCTAssertEqual(decoded, redecodedObjects) - - if let plistData { - let decodedPlistObjects = try! PropertyListDecoder().decode(T.self, from: plistData) - XCTAssertEqual(decoded, decodedPlistObjects) + do { + let reencodedData = try encoder.encode(decoded) + let redecodedObjects = try decoder.decode(T.self, from: reencodedData) + XCTAssertEqual(decoded, redecodedObjects) + + if let plistData { + let decodedPlistObjects = try PropertyListDecoder().decode(T.self, from: plistData) + XCTAssertEqual(decoded, decodedPlistObjects) + + } + } + catch { + XCTFail("Pass test \"\(name) failed with error: \(error)") } } } @@ -2523,6 +2529,10 @@ extension JSONEncoderTests { _run_passTest(name: "pass13", type: JSONPass.Test13.self) _run_passTest(name: "pass14", type: JSONPass.Test14.self) _run_passTest(name: "pass15", type: JSONPass.Test15.self) + // FIXME: Fix platform-specific crash on Windows, skipping test case for now +#if !os(Windows) + _run_passTest(name: "pass16", type: JSONPass.Test16.self) +#endif } func test_json5PassJSONFiles() { @@ -2587,6 +2597,7 @@ extension JSONEncoderTests { _run_failTest(name: "fail39", type: JSONFail.Test39.self) _run_failTest(name: "fail40", type: JSONFail.Test40.self) _run_failTest(name: "fail41", type: JSONFail.Test41.self) + _run_failTest(name: "fail42", type: JSONFail.Test42.self) } @@ -4368,6 +4379,12 @@ extension JSONPass { } } +extension JSONPass { + struct Test16: Codable, Equatable { + var nestedArray: [Test16]? + } +} + enum JSONFail { typealias Test1 = String typealias Test2 = [String] @@ -4409,6 +4426,7 @@ enum JSONFail { typealias Test39 = [String:String] typealias Test40 = [String:String] typealias Test41 = [String:String] + typealias Test42 = JSONPass.Test16 } enum JSON5Pass { } diff --git a/Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json b/Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json new file mode 100644 index 000000000..4263f00ee --- /dev/null +++ b/Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json @@ -0,0 +1,1027 @@ +{"nestedArray": [{ + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + +]} diff --git a/Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json b/Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json new file mode 100644 index 000000000..1d0f0c9b5 --- /dev/null +++ b/Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json @@ -0,0 +1,1025 @@ +{ + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + { + "nestedArray":[ + + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} From bfc4580760814fe5f883c6694e07497cc8a26d9c Mon Sep 17 00:00:00 2001 From: Christopher Thielen <77445+cthielen@users.noreply.github.com> Date: Tue, 20 May 2025 14:38:34 -0700 Subject: [PATCH 4/9] Update code formatting on SF-0011 with more readable indentation and generic parameters (#1305) --- .../0011-concurrency-safe-notifications.md | 145 ++++++++---------- 1 file changed, 68 insertions(+), 77 deletions(-) diff --git a/Proposals/0011-concurrency-safe-notifications.md b/Proposals/0011-concurrency-safe-notifications.md index d1d50e8c1..00d7bccd3 100644 --- a/Proposals/0011-concurrency-safe-notifications.md +++ b/Proposals/0011-concurrency-safe-notifications.md @@ -209,36 +209,37 @@ For `MainActorMessage`: @available(FoundationPreview 0.5, *) extension NotificationCenter { // e.g. addObserver(of: workspace, for: .willLaunchApplication) { message in ... } - public func addObserver(of subject: M.Subject, - for identifier: I, - using observer: @escaping @MainActor (M) -> Void) - -> ObservationToken where I.MessageType == M, - M.Subject: AnyObject + public func addObserver( + of subject: Message.Subject, + for identifier: Identifier, + using observer: @escaping @MainActor (Message) -> Void + ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: AnyObject - public func addObserver(of subject: M.Subject, - for identifier: I, - using observer: @escaping @MainActor (M) -> Void) - -> ObservationToken where I.MessageType == M, - M.Subject: Identifiable, - M.Subject.ID == ObjectIdentifier + public func addObserver( + of subject: Message.Subject, + for identifier: Identifier, + using observer: @escaping @MainActor (Message) -> Void + ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier // e.g. addObserver(of: NSWorkspace.self, for: .willLaunchApplication) { message in ... } - public func addObserver(of subject: M.Subject.Type, - for identifier: I, - using observer: @escaping @MainActor (M) -> Void) - -> ObservationToken where I.MessageType == M + public func addObserver( + of subject: Message.Subject.Type, + for identifier: Identifier, + using observer: @escaping @MainActor (Message) -> Void + ) -> ObservationToken where Identifier.MessageType == Message // e.g. addObserver(for: NSWorkspace.WillLaunchApplication.self) { message in ... } - public func addObserver(of subject: M.Subject? = nil, - for messageType: M.Type, - using observer: @escaping @MainActor (M) -> Void) - -> ObservationToken where M.Subject: AnyObject - - public func addObserver(of subject: M.Subject? = nil, - for messageType: M.Type, - using observer: @escaping @MainActor (M) -> Void) - -> ObservationToken where M.Subject: Identifiable, - M.Subject.ID == ObjectIdentifier + public func addObserver( + of subject: Message.Subject? = nil, + for messageType: Message.Type, + using observer: @escaping @MainActor (Message) -> Void + ) -> ObservationToken where Message.Subject: AnyObject + + public func addObserver( + of subject: Message.Subject? = nil, + for messageType: Message.Type, + using observer: @escaping @MainActor (Message) -> Void + ) -> ObservationToken where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier } ``` @@ -247,34 +248,35 @@ And for `AsyncMessage`: ```swift @available(FoundationPreview 0.5, *) extension NotificationCenter { - public func addObserver(of subject: M.Subject, - for identifier: I, - using observer: @escaping @Sendable (M) async -> Void) - -> ObservationToken where I.MessageType == M, - M.Subject: AnyObject - - public func addObserver(of subject: M.Subject, - for identifier: I, - using observer: @escaping @Sendable (M) async -> Void) - -> ObservationToken where I.MessageType == M, - M.Subject: Identifiable, - M.Subject.ID == ObjectIdentifier - - public func addObserver(of subject: M.Subject.Type, - for identifier: I, - using observer: @escaping @Sendable (M) async -> Void) - -> ObservationToken where I.MessageType == M + public func addObserver( + of subject: Message.Subject, + for identifier: Identifier, + using observer: @escaping @Sendable (Message) async -> Void + ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: AnyObject + + public func addObserver( + of subject: Message.Subject, + for identifier: Identifier, + using observer: @escaping @Sendable (Message) async -> Void + ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier + + public func addObserver( + of subject: Message.Subject.Type, + for identifier: Identifier, + using observer: @escaping @Sendable (Message) async -> Void + ) -> ObservationToken where Identifier.MessageType == Message - public func addObserver(of subject: M.Subject? = nil, - for messageType: M.Type, - using observer: @escaping @Sendable (M) async -> Void) - -> ObservationToken where M.Subject: AnyObject + public func addObserver( + of subject: Message.Subject? = nil, + for messageType: Message.Type, + using observer: @escaping @Sendable (Message) async -> Void + ) -> ObservationToken where Message.Subject: AnyObject - public func addObserver(of subject: M.Subject? = nil, - for messageType: M.Type, - using observer: @escaping @Sendable (M) async -> Void) - -> ObservationToken where M.Subject: Identifiable, - M.Subject.ID == ObjectIdentifier + public func addObserver( + of subject: Message.Subject? = nil, + for messageType: Message.Type, + using observer: @escaping @Sendable (Message) async -> Void + ) -> ObservationToken where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier } ``` @@ -302,40 +304,31 @@ extension NotificationCenter { of subject: Message.Subject, for identifier: Identifier, bufferSize limit: Int = 10 - ) - -> some AsyncSequence where Identifier.MessageType == Message, - Message.Subject: AnyObject + ) -> some AsyncSequence where Identifier.MessageType == Message, Message.Subject: AnyObject public func messages( of subject: Message.Subject, for identifier: Identifier, bufferSize limit: Int = 10 - ) - -> some AsyncSequence where Identifier.MessageType == Message, - Message.Subject: Identifiable, - Message.Subject.ID == ObjectIdentifier {} + ) -> some AsyncSequence where Identifier.MessageType == Message, Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier {} public func messages( of subject: Message.Subject.Type, for identifier: Identifier, bufferSize limit: Int = 10 - ) - -> some AsyncSequence where Identifier.MessageType == Message + ) -> some AsyncSequence where Identifier.MessageType == Message public func messages( of subject: Message.Subject? = nil, for messageType: Message.Type, bufferSize limit: Int = 10 - ) - -> some AsyncSequence where Message.Subject: AnyObject + ) -> some AsyncSequence where Message.Subject: AnyObject public func messages( of subject: Message.Subject? = nil, for messageType: Message.Type, bufferSize limit: Int = 10 - ) - -> some AsyncSequence where Message.Subject: Identifiable, - Message.Subject.ID == ObjectIdentifier + ) -> some AsyncSequence where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier } ``` @@ -366,27 +359,25 @@ extension NotificationCenter { // MainActorMessage post() @MainActor - public func post(_ message: M, subject: M.Subject) - where M.Subject: AnyObject + public func post(_ message: Message, subject: Message.Subject) + where Message.Subject: AnyObject @MainActor - public func post(_ message: M, subject: M.Subject) - where M.Subject: Identifiable, - M.Subject.ID == ObjectIdentifier + public func post(_ message: Message, subject: Message.Subject) + where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier @MainActor - public func post(_ message: M, subject: M.Subject.Type = M.Subject.self) + public func post(_ message: Message, subject: Message.Subject.Type = Message.Subject.self) // AsyncMessage post() - public func post(_ message: M, subject: M.Subject) - where M.Subject: AnyObject + public func post(_ message: Message, subject: Message.Subject) + where Message.Subject: AnyObject - public func post(_ message: M, subject: M.Subject) - where M.Subject: Identifiable, - M.Subject.ID == ObjectIdentifier + public func post(_ message: Message, subject: Message.Subject) + where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier - public func post(_ message: M, subject: M.Subject.Type = M.Subject.self) + public func post(_ message: Message, subject: Message.Subject.Type = Message.Subject.self) } ``` From a8217de8ae7d0f982a0121055954ff625fe7f611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=B0=E0=A4=A8=20=E0=A4=AE=E0=A4=BF=E0=A4=B6?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0?= <103079589+karan-misra@users.noreply.github.com> Date: Thu, 22 May 2025 17:39:06 -0700 Subject: [PATCH 5/9] Locale.preferredLocales (#1291) * Locale.preferredLocales Initial proposal * Update Proposals/0026-preferredLocales.md Fix link Co-authored-by: Tina L <49205802+itingliu@users.noreply.github.com> --------- Co-authored-by: Tina L <49205802+itingliu@users.noreply.github.com> --- Proposals/0026-preferredLocales.md | 77 ++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Proposals/0026-preferredLocales.md diff --git a/Proposals/0026-preferredLocales.md b/Proposals/0026-preferredLocales.md new file mode 100644 index 000000000..d625a28d4 --- /dev/null +++ b/Proposals/0026-preferredLocales.md @@ -0,0 +1,77 @@ +# Locale.preferredLocales + +* Proposal: [SF-0026](0026-preferredLocales.md) +* Authors: [करन मिश्र · Karan Miśra](https://github.com/karan-misra) +* Review Manager: TBD +* Status: **Awaiting review** +* Review: ([pitch](https://forums.swift.org/t/pitch-introduce-locale-preferredlocales/79900)) + +## Introduction + +Add `Locale.preferredLocales` as an alternative to `Locale.preferredLanguages` that returns `[Locale]` instead of `[String]`. + +## Motivation + +Currently, `Locale.preferredLanguages` is the only way to retrieve the list of languages that the user has specified in Language & Region settings. This follows its predecessor `+[NSLocale preferredLanguages]` and returns an array of `String`s instead of `Locale`s. Processing and manipulating strings is complex and errorprone for clients. + +This proposal introduces `Locale.preferredLocales` as a way to retrieve the same information, but in the form of an array of `Locale`s which will allow clients to use the information more easily and with fewer errors, specifically when used to customize the presentation of data within their apps such that content in the user’s preferred languages is more prominent. + +## Proposed solution + +We propose adding `preferredLocales` as a static variable on `Locale`, similarly to `preferredLanguages`. One of the primary use cases is to allow apps to build language selection menus in which the user’s preferred locales are bubbled up to the top. This can be achieved with the proposed `preferredLocales` API as follows: + +```swift +// When building a language selection menu, `matchedLocales` would be shown at the top, and `otherLocales` would be shown below, with a visual divider. +var matchedLocales = [] +var otherLocales = [] +let availableLocales = // ... array of Locale objects ... +for locale in availableLocales { + var foundMatch = false + for preferredLocale in preferredLocales { + if locale.language.isEquivalent(to: preferredLocale.language) { + matchedLocales.append(locale) + foundMatch = true + break + } + } + if !foundMatch { + otherLocales.append(locale) + } +} +``` + +## Detailed design + +```swift +public struct Locale : Hashable, Equatable, Sendable { + + /// Returns a list of the user’s preferred locales, as specified in Language & Region settings, taking into account any per-app language overrides. + @available(FoundationPreview 6.2, *) + public static var preferredLocales: [Locale] +} +``` + +## Source compatibility + +There is no impact on source compatibility or existing code. + +## Implications on adoption + +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility. + +## Future directions + +In order to further support the use case of building language selection UIs, we can consider adding convenience functions on `Locale` that allow sorting and splitting a list of available `Locale`s into `preferred` and `remaining`, which can then be used to directly populate the UI. + +```swift +public static func sorted(_ available: [Locale]) -> (preferred: [Locale], remaining: [Locale]) + ``` + +We can also consider adding APIs that work with `Locale.Language` in addition to `Locale` since in many use cases, the developer is handling a list of languages does not need the additional functionality in `Locale`. + +Lastly, we can choose to deprecate `Locale.preferredLanguages` since it returns the same information but using `String` which is not a good container for a language identifier and leads to incorrect usage. + +## Alternatives considered + +* Naming-wise, another possibility was `Locale.preferred`. However, following the current naming convention, this would be confused as returning `Locale` and not `[Locale]`. Additionally, it would be best to keep `Locale.preferred` open in case we need a way to get the first, most preferred `Locale` in the future. +* Deprecate `preferredLanguages` and encourage developers to only use `preferredLocales`. From 4dc19f0e78c11ae8bd8e73d9b7af3c0bc71a7502 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Fri, 23 May 2025 09:36:49 -0700 Subject: [PATCH 6/9] Introduce preferredLocales API (#1315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Karan Miśra --- Sources/FoundationEssentials/Locale/Locale.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/Locale/Locale.swift b/Sources/FoundationEssentials/Locale/Locale.swift index 3eb780564..3c0620674 100644 --- a/Sources/FoundationEssentials/Locale/Locale.swift +++ b/Sources/FoundationEssentials/Locale/Locale.swift @@ -554,15 +554,24 @@ public struct Locale : Hashable, Equatable, Sendable { // MARK: - // - /// Returns a list of the user's preferred languages. + /// Returns a list of the user's preferred languages, as specified in Language & Region settings, taking into account any per-app language overrides. /// /// - note: `Bundle` is responsible for determining the language that your application will run in, based on the result of this API and combined with the languages your application supports. /// - seealso: `Bundle.preferredLocalizations(from:)` /// - seealso: `Bundle.preferredLocalizations(from:forPreferences:)` + /// - seealso: `Locale.preferredLocales` public static var preferredLanguages: [String] { LocaleCache.cache.preferredLanguages(forCurrentUser: false) } + /// Returns a list of the user’s preferred locales, as specified in Language & Region settings, taking into account any per-app language overrides. + @available(FoundationPreview 6.2, *) + public static var preferredLocales: [Locale] { + return self.preferredLanguages.compactMap { + Locale(identifier: $0) + } + } + private static let languageCodeKey = "kCFLocaleLanguageCodeKey" private static let scriptCodeKey = "kCFLocaleScriptCodeKey" private static let countryCodeKey = "kCFLocaleCountryCodeKey" From 1eed9aa6a115a01eedae8dda26a14a26348cadb8 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Fri, 23 May 2025 09:47:58 -0700 Subject: [PATCH 7/9] Revert "Fix #1176 JSON Decoder and Encoder limit disagreement (#1242)" (#1306) This reverts commit 0f62460907181a3238ff72c54acfbf84b13b5fac. --- .../JSON/JSONScanner.swift | 5 +- .../JSON/JSONWriter.swift | 15 +- .../JSONEncoderTests.swift | 32 +- .../Resources/JSON/fail/fail42.json | 1027 ----------------- .../Resources/JSON/pass/pass16.json | 1025 ---------------- 5 files changed, 21 insertions(+), 2083 deletions(-) delete mode 100644 Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json delete mode 100644 Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json diff --git a/Sources/FoundationEssentials/JSON/JSONScanner.swift b/Sources/FoundationEssentials/JSON/JSONScanner.swift index 9f368cb97..f78c3637b 100644 --- a/Sources/FoundationEssentials/JSON/JSONScanner.swift +++ b/Sources/FoundationEssentials/JSON/JSONScanner.swift @@ -265,7 +265,6 @@ internal struct JSONScanner { var reader: DocumentReader var depth: Int = 0 var partialMap = JSONPartialMapData() - private static let maximumRecursionDepth = 512 internal struct Options { var assumesTopLevelDictionary = false @@ -413,7 +412,7 @@ internal struct JSONScanner { mutating func scanArray() throws { let firstChar = reader.read() precondition(firstChar == ._openbracket) - guard self.depth < Self.maximumRecursionDepth else { + guard self.depth < 512 else { throw JSONError.tooManyNestedArraysOrDictionaries(location: reader.sourceLocation(atOffset: 1)) } self.depth &+= 1 @@ -471,7 +470,7 @@ internal struct JSONScanner { mutating func scanObject() throws { let firstChar = self.reader.read() precondition(firstChar == ._openbrace) - guard self.depth < Self.maximumRecursionDepth else { + guard self.depth < 512 else { throw JSONError.tooManyNestedArraysOrDictionaries(location: reader.sourceLocation(atOffset: -1)) } try scanObject(withoutBraces: false) diff --git a/Sources/FoundationEssentials/JSON/JSONWriter.swift b/Sources/FoundationEssentials/JSON/JSONWriter.swift index b8ff000a7..2ecf8b7b5 100644 --- a/Sources/FoundationEssentials/JSON/JSONWriter.swift +++ b/Sources/FoundationEssentials/JSON/JSONWriter.swift @@ -29,9 +29,6 @@ internal struct JSONWriter { } mutating func serializeJSON(_ value: JSONEncoderValue, depth: Int = 0) throws { - guard depth < Self.maximumRecursionDepth else { - throw JSONError.tooManyNestedArraysOrDictionaries() - } switch value { case .string(let str): serializeString(str) @@ -175,6 +172,10 @@ internal struct JSONWriter { } mutating func serializeArray(_ array: [JSONEncoderValue], depth: Int) throws { + guard depth < Self.maximumRecursionDepth else { + throw JSONError.tooManyNestedArraysOrDictionaries() + } + writer(ascii: ._openbracket) if pretty { writer(ascii: ._newline) @@ -203,6 +204,10 @@ internal struct JSONWriter { } mutating func serializePreformattedByteArray(_ bytes: [UInt8], _ lengths: [Int], depth: Int) throws { + guard depth < Self.maximumRecursionDepth else { + throw JSONError.tooManyNestedArraysOrDictionaries() + } + writer(ascii: ._openbracket) if pretty { writer(ascii: ._newline) @@ -237,6 +242,10 @@ internal struct JSONWriter { } mutating func serializeObject(_ dict: [String:JSONEncoderValue], depth: Int) throws { + guard depth < Self.maximumRecursionDepth else { + throw JSONError.tooManyNestedArraysOrDictionaries() + } + writer(ascii: ._openbrace) if pretty { writer(ascii: ._newline) diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 3b18e0baa..6697df493 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -2492,19 +2492,13 @@ extension JSONEncoderTests { prettyPrintEncoder.outputFormatting = .prettyPrinted for encoder in [JSONEncoder(), prettyPrintEncoder] { - do { - let reencodedData = try encoder.encode(decoded) - let redecodedObjects = try decoder.decode(T.self, from: reencodedData) - XCTAssertEqual(decoded, redecodedObjects) - - if let plistData { - let decodedPlistObjects = try PropertyListDecoder().decode(T.self, from: plistData) - XCTAssertEqual(decoded, decodedPlistObjects) - - } - } - catch { - XCTFail("Pass test \"\(name) failed with error: \(error)") + let reencodedData = try! encoder.encode(decoded) + let redecodedObjects = try! decoder.decode(T.self, from: reencodedData) + XCTAssertEqual(decoded, redecodedObjects) + + if let plistData { + let decodedPlistObjects = try! PropertyListDecoder().decode(T.self, from: plistData) + XCTAssertEqual(decoded, decodedPlistObjects) } } } @@ -2529,10 +2523,6 @@ extension JSONEncoderTests { _run_passTest(name: "pass13", type: JSONPass.Test13.self) _run_passTest(name: "pass14", type: JSONPass.Test14.self) _run_passTest(name: "pass15", type: JSONPass.Test15.self) - // FIXME: Fix platform-specific crash on Windows, skipping test case for now -#if !os(Windows) - _run_passTest(name: "pass16", type: JSONPass.Test16.self) -#endif } func test_json5PassJSONFiles() { @@ -2597,7 +2587,6 @@ extension JSONEncoderTests { _run_failTest(name: "fail39", type: JSONFail.Test39.self) _run_failTest(name: "fail40", type: JSONFail.Test40.self) _run_failTest(name: "fail41", type: JSONFail.Test41.self) - _run_failTest(name: "fail42", type: JSONFail.Test42.self) } @@ -4379,12 +4368,6 @@ extension JSONPass { } } -extension JSONPass { - struct Test16: Codable, Equatable { - var nestedArray: [Test16]? - } -} - enum JSONFail { typealias Test1 = String typealias Test2 = [String] @@ -4426,7 +4409,6 @@ enum JSONFail { typealias Test39 = [String:String] typealias Test40 = [String:String] typealias Test41 = [String:String] - typealias Test42 = JSONPass.Test16 } enum JSON5Pass { } diff --git a/Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json b/Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json deleted file mode 100644 index 4263f00ee..000000000 --- a/Tests/FoundationEssentialsTests/Resources/JSON/fail/fail42.json +++ /dev/null @@ -1,1027 +0,0 @@ -{"nestedArray": [{ - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - -]} diff --git a/Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json b/Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json deleted file mode 100644 index 1d0f0c9b5..000000000 --- a/Tests/FoundationEssentialsTests/Resources/JSON/pass/pass16.json +++ /dev/null @@ -1,1025 +0,0 @@ -{ - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - { - "nestedArray":[ - - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] -} From a456c0baa06dc68e143a2bd5d40b5b94d985ad76 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Fri, 23 May 2025 13:58:00 -0700 Subject: [PATCH 8/9] (151882766) Fixup documentation for new AttributedString APIs (#1316) --- .../AttributedString/AttributeContainer.swift | 10 ++++--- .../AttributedString/AttributeScope.swift | 1 + .../AttributedString+IndexTracking.swift | 30 +++++++++++-------- .../AttributedString+IndexValidity.swift | 18 +++++++++++ .../AttributedString+UTF16View.swift | 3 ++ .../AttributedString+UTF8View.swift | 5 +++- .../DiscontiguousAttributedSubstring.swift | 26 ++++++++++++++++ 7 files changed, 75 insertions(+), 18 deletions(-) diff --git a/Sources/FoundationEssentials/AttributedString/AttributeContainer.swift b/Sources/FoundationEssentials/AttributedString/AttributeContainer.swift index 16b268496..10c2dc35c 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributeContainer.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributeContainer.swift @@ -108,7 +108,9 @@ extension AttributeContainer { @available(FoundationPreview 6.2, *) extension AttributeContainer { - /// Returns an attribute container storing only the attributes in `self` with the `inheritedByAddedText` property set to `true` + /// Returns a copy of the attribute container with only attributes that specify the provided inheritance behavior. + /// - Parameter inheritedByAddedText: An `inheritedByAddedText` value to filter. Attributes matching this value are included in the returned container. + /// - Returns: A copy of the attribute container with only attributes whose `inheritedByAddedText` property matches the provided value. public func filter(inheritedByAddedText: Bool) -> AttributeContainer { var storage = self.storage for (key, value) in storage.contents { @@ -120,9 +122,9 @@ extension AttributeContainer { return AttributeContainer(storage) } - /// Returns an attribute container storing only the attributes in `self` with a matching run boundary property - /// - /// Note: if `nil` is provided then only attributes not bound to any particular boundary will be returned + /// Returns a copy of the attribute container with only attributes that have the provided run boundaries. + /// - Parameter runBoundaries: The required `runBoundaries` value of the filtered attributes. If `nil` is provided, only attributes not bound to any specific boundary will be returned. + /// - Returns: A copy of the attribute container with only attributes whose `runBoundaries` property matches the provided value. public func filter(runBoundaries: AttributedString.AttributeRunBoundaries?) -> AttributeContainer { var storage = self.storage for (key, value) in storage.contents { diff --git a/Sources/FoundationEssentials/AttributedString/AttributeScope.swift b/Sources/FoundationEssentials/AttributedString/AttributeScope.swift index 656bf27cb..2fac17426 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributeScope.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributeScope.swift @@ -206,6 +206,7 @@ extension AttributeScope { Self.scopeDescription.markdownAttributes } + /// A list of all attribute keys contained within this scope and any sub-scopes. @available(FoundationPreview 6.2, *) public static var attributeKeys: some Sequence { Self.scopeDescription.attributes.values diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+IndexTracking.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+IndexTracking.swift index c4ac53797..c4646fc6c 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+IndexTracking.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+IndexTracking.swift @@ -79,10 +79,12 @@ extension AttributedString.Guts { extension AttributedString { // MARK: inout API - /// Tracks the location of the provided range throughout the mutation closure, updating the provided range to one that represents the same effective locations after the mutation. If updating the provided range is not possible (tracking failed) then this function will fatal error. Use the Optional-returning variants to provide custom fallback behavior. + /// Tracks the location of the provided range throughout the mutation closure, updating the provided range to one that represents the same effective locations after the mutation. + /// + /// If updating the provided range is not possible (tracking failed) then this function will fatal error. Use the `Optional`-returning variants to provide custom fallback behavior. /// - Parameters: - /// - range: a range to track throughout the `body` closure - /// - body: a mutating operation, or set of operations, to perform on the value of `self`. The value of `self` is provided to the closure as an `inout AttributedString` that the closure should mutate directly. Do not capture the value of `self` in the provided closure - the closure should mutate the provided `inout` copy. + /// - range: A range to track throughout the `body` closure. + /// - body: A mutating operation, or set of operations, to perform on the value of `self`. The value of `self` is provided to the closure as an `inout AttributedString` that the closure should mutate directly. Do not capture the value of `self` in the provided closure - the closure should mutate the provided `inout` copy. public mutating func transform(updating range: inout Range, body: (inout AttributedString) throws(E) -> Void) throws(E) -> Void { guard let result = try self.transform(updating: range, body: body) else { fatalError("The provided mutation body did not allow for maintaining index tracking. Ensure that your mutation body mutates the provided AttributedString instead of replacing it with a different AttributedString or use the non-inout version of transform(updating:body:) which returns an Optional value to provide fallback behavior.") @@ -90,10 +92,12 @@ extension AttributedString { range = result } - /// Tracks the location of the provided ranges throughout the mutation closure, updating them to new ranges that represent the same effective locations after the mutation. If updating the provided ranges is not possible (tracking failed) then this function will fatal error. Use the Optional-returning variants to provide custom fallback behavior. + /// Tracks the location of the provided ranges throughout the mutation closure, updating them to new ranges that represent the same effective locations after the mutation. + /// + /// If updating the provided ranges is not possible (tracking failed) then this function will fatal error. Use the `Optional`-returning variants to provide custom fallback behavior. /// - Parameters: - /// - ranges: a list of ranges to track throughout the `body` closure. The updated array (after the function is called) is guaranteed to be the same size as the provided array. Updated ranges are located at the same indices as their respective original ranges in the input `ranges` array. - /// - body: a mutating operation, or set of operations, to perform on the value of `self`. The value of `self` is provided to the closure as an `inout AttributedString` that the closure should mutate directly. Do not capture the value of `self` in the provided closure - the closure should mutate the provided `inout` copy. + /// - ranges: A list of ranges to track throughout the `body` closure. The updated array (after the function is called) is guaranteed to be the same size as the provided array. Updated ranges are located at the same indices as their respective original ranges in the input `ranges` array. + /// - body: A mutating operation, or set of operations, to perform on the value of `self`. The value of `self` is provided to the closure as an `inout AttributedString` that the closure should mutate directly. Do not capture the value of `self` in the provided closure - the closure should mutate the provided `inout` copy. public mutating func transform(updating ranges: inout [Range], body: (inout AttributedString) throws(E) -> Void) throws(E) -> Void { guard let result = try self.transform(updating: ranges, body: body) else { fatalError("The provided mutation body did not allow for maintaining index tracking. Ensure that your mutation body mutates the provided AttributedString instead of replacing it with a different AttributedString or use the non-inout version of transform(updating:body:) which returns an Optional value to provide fallback behavior.") @@ -103,20 +107,20 @@ extension AttributedString { // MARK: Optional-returning API - /// Tracks the location of the provided range throughout the mutation closure, returning a new, updated range that represents the same effective locations after the mutation + /// Tracks the location of the provided range throughout the mutation closure, returning a new, updated range that represents the same effective locations after the mutation. /// - Parameters: - /// - range: a range to track throughout the `mutation` block - /// - mutation: a mutating operation, or set of operations, to perform on this `AttributedString` - /// - Returns: the updated `Range` that is valid after the mutation has been performed, or `nil` if the mutation performed does not allow for tracking to succeed (such as replacing the provided inout variable with an entirely different AttributedString) + /// - range: A range to track throughout the `body` block. + /// - body: A mutating operation, or set of operations, to perform on this `AttributedString`. + /// - Returns: the updated `Range` that is valid after the mutation has been performed, or `nil` if the mutation performed does not allow for tracking to succeed (such as replacing the provided inout variable with an entirely different `AttributedString`). public mutating func transform(updating range: Range, body: (inout AttributedString) throws(E) -> Void) throws(E) -> Range? { try self.transform(updating: [range], body: body)?.first } /// Tracks the location of the provided ranges throughout the mutation closure, returning a new, updated range that represents the same effective locations after the mutation /// - Parameters: - /// - index: an index to track throughout the `mutation` block - /// - mutation: a mutating operation, or set of operations, to perform on this `AttributedString` - /// - Returns: the updated `Range`s that is valid after the mutation has been performed, or `nil` if the mutation performed does not allow for tracking to succeed (such as replacing the provided inout variable with an entirely different AttributedString). When the return value is non-nil, the returned array is guaranteed to be the same size as the provided array with updated ranges at the same Array indices as their respective original ranges in the input array. + /// - ranges: Ranges to track throughout the `body` block. + /// - body: A mutating operation, or set of operations, to perform on this `AttributedString`. + /// - Returns: the updated `Range`s that are valid after the mutation has been performed or `nil` if the mutation performed does not allow for tracking to succeed (such as replacing the provided inout variable with an entirely different `AttributedString`). When the return value is non-`nil`, the returned array is guaranteed to be the same size as the provided array with updated ranges at the same indices as their respective original ranges in the input array. public mutating func transform(updating ranges: [Range], body: (inout AttributedString) throws(E) -> Void) throws(E) -> [Range]? { precondition(!ranges.isEmpty, "Cannot update an empty array of ranges") diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift index ec35ca236..8b617aaab 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift @@ -45,12 +45,18 @@ extension AttributedString.Guts { @available(FoundationPreview 6.2, *) extension AttributedString.Index { + /// Indicates whether the index is valid for use with the provided attributed string. + /// - Parameter text: An attributed string used to validate the index. + /// - Returns: `true` when the index is valid for use with the provided attributed string; otherwise, false. An index is valid if it is both within the bounds of the attributed string and was produced from the provided string without any intermediate mutations. public func isValid(within text: some AttributedStringProtocol) -> Bool { self._version == text.__guts.version && self >= text.startIndex && self < text.endIndex } + /// Indicates whether the index is valid for use with the provided discontiguous attributed string. + /// - Parameter text: A discontiguous attributed string used to validate the index. + /// - Returns: `true` when the index is valid for use with the provided discontiguous attributed string; otherwise, false. An index is valid if it is both within the bounds of the discontigous attributed string and was produced from the provided string without any intermediate mutations. public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool { self._version == text._guts.version && text._indices.contains(self._value) @@ -59,6 +65,9 @@ extension AttributedString.Index { @available(FoundationPreview 6.2, *) extension Range { + /// Indicates whether the range is valid for use with the provided attributed string. + /// - Parameter text: An attributed string used to validate the range. + /// - Returns: `true` when the range is valid for use with the provided attributed string; otherwise, false. A range is valid if its lower and upper bounds are each either valid in the attributed string or equivalent to the string's `endIndex`. public func isValid(within text: some AttributedStringProtocol) -> Bool { // Note: By nature of Range's lowerBound <= upperBound requirement, this is also sufficient to determine that lowerBound <= endIndex && upperBound >= startIndex self.lowerBound._version == text.__guts.version && @@ -67,6 +76,9 @@ extension Range { self.upperBound <= text.endIndex } + /// Indicates whether the range is valid for use with the provided discontiguous attributed string. + /// - Parameter text: A discontiguous attributed string used to validate the range. + /// - Returns: `true` when the range is valid for use with the provided discontiguous attributed string; otherwise, false. A range is valid if its lower and upper bounds are each either valid in the discontiguous attributed string or equivalent to the string's `endIndex`. public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool { let endIndex = text._indices.ranges.last?.upperBound return self.lowerBound._version == text._guts.version && @@ -78,12 +90,18 @@ extension Range { @available(FoundationPreview 6.2, *) extension RangeSet { + /// Indicates whether the range set is valid for use with the provided attributed string. + /// - Parameter text: An attributed string used to validate the range set. + /// - Returns: `true` when the range set is valid for use with the provided attributed string; otherwise, false. A range set is valid if each of its ranges are valid in the attributed string. public func isValid(within text: some AttributedStringProtocol) -> Bool { self.ranges.allSatisfy { $0.isValid(within: text) } } + /// Indicates whether the range set is valid for use with the provided discontiguous attributed string. + /// - Parameter text: A discontigious attributed string used to validate the range set. + /// - Returns: `true` when the range set is valid for use with the provided discontiguous attributed string; otherwise, false. A range set is valid if each of its ranges are valid in the discontiguous attributed string. public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool { self.ranges.allSatisfy { $0.isValid(within: text) diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift index b732e4e3c..a9566bebd 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift @@ -20,6 +20,7 @@ internal import _FoundationCollections @available(FoundationPreview 6.2, *) extension AttributedString { + /// A view of an attributed string’s contents as a collection of UTF-16 code units. public struct UTF16View: Sendable { internal var _guts: Guts internal var _range: Range @@ -35,6 +36,7 @@ extension AttributedString { } } + /// A view of the attributed string’s contents as a collection of UTF-16 code units. public var utf16: UTF16View { UTF16View(_guts) } @@ -42,6 +44,7 @@ extension AttributedString { @available(FoundationPreview 6.2, *) extension AttributedSubstring { + /// A view of the attributed substring's contents as a collection of UTF-16 code units. public var utf16: AttributedString.UTF16View { AttributedString.UTF16View(_guts, in: _range) } diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift index 1a993e7fc..7f68cf8b2 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift @@ -20,6 +20,7 @@ internal import _FoundationCollections @available(FoundationPreview 6.2, *) extension AttributedString { + /// A view of an attributed string’s contents as a collection of UTF-8 code units. public struct UTF8View: Sendable { internal var _guts: Guts internal var _range: Range @@ -34,7 +35,8 @@ extension AttributedString { _range = range } } - + + /// A view of the attributed string’s contents as a collection of UTF-8 code units. public var utf8: UTF8View { UTF8View(_guts) } @@ -42,6 +44,7 @@ extension AttributedString { @available(FoundationPreview 6.2, *) extension AttributedSubstring { + /// A view of the attributed substring's contents as a collection of UTF-8 code units. public var utf8: AttributedString.UTF8View { AttributedString.UTF8View(_guts, in: _range) } diff --git a/Sources/FoundationEssentials/AttributedString/DiscontiguousAttributedSubstring.swift b/Sources/FoundationEssentials/AttributedString/DiscontiguousAttributedSubstring.swift index e73fcb907..649ceb730 100644 --- a/Sources/FoundationEssentials/AttributedString/DiscontiguousAttributedSubstring.swift +++ b/Sources/FoundationEssentials/AttributedString/DiscontiguousAttributedSubstring.swift @@ -18,6 +18,7 @@ internal import _RopeModule internal import _FoundationCollections #endif +/// A discontiguous portion of an attributed string. @dynamicMemberLookup @available(FoundationPreview 6.2, *) public struct DiscontiguousAttributedSubstring: Sendable { @@ -42,6 +43,7 @@ public struct DiscontiguousAttributedSubstring: Sendable { @available(FoundationPreview 6.2, *) extension DiscontiguousAttributedSubstring { + /// The underlying attributed string that the discontiguous attributed substring derives from. public var base: AttributedString { return AttributedString(_guts) } @@ -124,6 +126,8 @@ extension DiscontiguousAttributedSubstring : AttributedStringAttributeMutation { } } + /// Returns a discontiguous substring of this discontiguous attributed string using a range to indicate the discontiguous substring bounds. + /// - Parameter bounds: A range that indicates the bounds of the discontiguous substring to return. public subscript(bounds: some RangeExpression) -> DiscontiguousAttributedSubstring { let characterView = AttributedString.CharacterView(_guts) let bounds = bounds.relative(to: characterView)._bstringRange @@ -133,6 +137,8 @@ extension DiscontiguousAttributedSubstring : AttributedStringAttributeMutation { preconditionFailure("Attributed string index range \(bounds) is out of bounds") } + /// Returns a discontiguous substring of this discontiguous attributed string using a set of ranges to indicate the discontiguous substring bounds. + /// - Parameter bounds: A set of ranges that indicate the bounds of the discontiguous substring to return. public subscript(bounds: RangeSet) -> DiscontiguousAttributedSubstring { let bounds = bounds._bstringIndices if bounds.ranges.isEmpty { @@ -151,14 +157,17 @@ extension DiscontiguousAttributedSubstring : AttributedStringAttributeMutation { @available(FoundationPreview 6.2, *) extension DiscontiguousAttributedSubstring { + /// The characters of the discontiguous attributed string, as a view into the underlying string. public var characters: DiscontiguousSlice { AttributedString.CharacterView(_guts)[_indices._attributedStringIndices(version: _guts.version)] } + /// The Unicode scalars of the discontiguous attributed string, as a view into the underlying string. public var unicodeScalars: DiscontiguousSlice { AttributedString.UnicodeScalarView(_guts)[_indices._attributedStringIndices(version: _guts.version)] } + /// The attributed runs of the discontiguous attributed string, as a view into the underlying string. public var runs: AttributedString.Runs { AttributedString.Runs(_guts, in: _indices) } @@ -166,6 +175,10 @@ extension DiscontiguousAttributedSubstring { @available(FoundationPreview 6.2, *) extension DiscontiguousAttributedSubstring { + /// Returns an attribute value that corresponds to an attributed string key. + /// + /// This subscript returns `nil` unless the specified attribute exists, and is present and identical for the entire discontiguous attributed substring. To find portions of an attributed string with consistent attributes, use the `runs` property. + /// Getting or setting stringwide attributes with this subscript has `O(n)` behavior in the worst case, where n is the number of runs. public subscript(_: K.Type) -> K.Value? where K.Value : Sendable { get { var result: AttributedString._AttributeValue? @@ -194,6 +207,10 @@ extension DiscontiguousAttributedSubstring { } } + /// Returns an attribute value that a key path indicates. + /// + /// This subscript returns `nil` unless the specified attribute exists, and is present and identical for the entire discontiguous attributed substring. To find portions of an attributed string with consistent attributes, use the `runs` property. + /// Getting or setting stringwide attributes with this subscript has `O(n)` behavior in the worst case, where n is the number of runs. @inlinable // Trivial implementation, allows callers to optimize away the keypath allocation public subscript( dynamicMember keyPath: KeyPath @@ -202,6 +219,7 @@ extension DiscontiguousAttributedSubstring { set { self[K.self] = newValue } } + /// Returns a scoped attribute container that a key path indicates. public subscript( dynamicMember keyPath: KeyPath ) -> ScopedAttributeContainer { @@ -247,6 +265,8 @@ extension DiscontiguousAttributedSubstring { @available(FoundationPreview 6.2, *) extension AttributedString { + /// Creates an attributed string from a discontiguous attributed substring. + /// - Parameter substring: A discontiguous attributed substring to create the new attributed string from. public init(_ substring: DiscontiguousAttributedSubstring) { let created = AttributedString.Guts() for range in substring._indices.ranges { @@ -261,6 +281,8 @@ extension AttributedString { @available(FoundationPreview 6.2, *) extension AttributedStringProtocol { + /// Returns a discontiguous substring of this attributed string using a set of ranges to indicate the discontiguous substring bounds. + /// - Parameter indices: A set of ranges that indicate the bounds of the discontiguous substring to return. public subscript(_ indices: RangeSet) -> DiscontiguousAttributedSubstring { let range = Range(uncheckedBounds: (startIndex, endIndex))._bstringRange let newIndices = indices._bstringIndices.intersection(RangeSet(range)) @@ -270,6 +292,8 @@ extension AttributedStringProtocol { @available(FoundationPreview 6.2, *) extension AttributedString { + /// Returns a discontiguous substring of this discontiguous attributed string using a set of ranges to indicate the discontiguous substring bounds. + /// - Parameter indices: A set of ranges that indicate the bounds of the discontiguous substring to return. public subscript(_ indices: RangeSet) -> DiscontiguousAttributedSubstring { get { let range = Range(uncheckedBounds: (startIndex, endIndex))._bstringRange @@ -312,6 +336,8 @@ extension AttributedString { } } + /// Removes the elements at the given indices. + /// - Parameter subranges: The indices of the elements to remove. public mutating func removeSubranges(_ subranges: RangeSet) { ensureUniqueReference() for range in subranges.ranges.lazy.reversed() { From 4d74f7a425b3edb48ebec04105a06eb99771a519 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Fri, 23 May 2025 15:26:30 -0700 Subject: [PATCH 9/9] (151862798) Resolve AttributedString attribute storage Sendable warnings (#1313) --- .../AttributedStringAttributeStorage.swift | 46 +++++++++++++++++++ .../AttributedStringCodable.swift | 6 ++- .../AttributedString/Conversion.swift | 6 ++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationEssentials/AttributedString/AttributedStringAttributeStorage.swift b/Sources/FoundationEssentials/AttributedString/AttributedStringAttributeStorage.swift index 0f9521de1..d10c84ff8 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedStringAttributeStorage.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedStringAttributeStorage.swift @@ -32,6 +32,21 @@ extension AttributedString { inheritedByAddedText = K.inheritedByAddedText invalidationConditions = K.invalidationConditions } + + #if FOUNDATION_FRAMEWORK + @inline(__always) + private static func _unsafeAssumeSendableRawValue(_ value: T) -> RawValue { + // Perform this cast in a separate function unaware of the T: Hashable constraint to avoid compiler warnings when performing the Hashable --> Hashable & Sendable cast + value as! RawValue + } + + fileprivate init(assumingSendable value: K.Value, for key: K.Type) { + _rawValue = Self._unsafeAssumeSendableRawValue(value) + runBoundaries = K.runBoundaries + inheritedByAddedText = K.inheritedByAddedText + invalidationConditions = K.invalidationConditions + } + #endif var isInvalidatedOnTextChange: Bool { invalidationConditions?.contains(.textChanged) ?? false @@ -61,6 +76,18 @@ extension AttributedString { return value } + #if FOUNDATION_FRAMEWORK + fileprivate func rawValueAssumingSendable( + as key: K.Type + ) -> K.Value { + // Dynamic cast instead of an identity cast to support bridging between attribute value types like NSColor/UIColor + guard let value = self._rawValue as? K.Value else { + preconditionFailure("Unable to read \(K.self) attribute: stored value of type \(type(of: self._rawValue)) is not key's value type (\(K.Value.self))") + } + return value + } + #endif + static func ==(left: Self, right: Self) -> Bool { func openEquatableLHS(_ leftValue: LeftValue) -> Bool { func openEquatableRHS(_ rightValue: RightValue) -> Bool { @@ -193,6 +220,25 @@ extension AttributedString._AttributeStorage { get { self[T.name]?.rawValue(as: T.self) } set { self[T.name] = .wrapIfPresent(newValue, for: T.self) } } + + #if FOUNDATION_FRAMEWORK + /// Stores & retrieves an attribute value bypassing the T.Value : Sendable constraint + /// + /// In general, callers should _always_ use the subscript that contains a T.Value : Sendable constraint + /// This subscript should only be used in contexts when callers are forced to work around the lack of an AttributedStringKey.Value : Sendable constraint and assume the values are Sendable (ex. during NSAttributedString conversion while iterating scopes) + subscript (assumingSendable attribute: T.Type) -> T.Value? { + get { + self[T.name]?.rawValueAssumingSendable(as: T.self) + } + set { + guard let newValue else { + self[T.name] = nil + return + } + self[T.name] = _AttributeValue(assumingSendable: newValue, for: T.self) + } + } + #endif subscript (_ attributeName: String) -> _AttributeValue? { get { self.contents[attributeName] } diff --git a/Sources/FoundationEssentials/AttributedString/AttributedStringCodable.swift b/Sources/FoundationEssentials/AttributedString/AttributedStringCodable.swift index db82eca5e..33f28ab47 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedStringCodable.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedStringCodable.swift @@ -228,7 +228,8 @@ extension AttributedString : CodableWithConfiguration { { let attributeEncoder = attributesContainer.superEncoder(forKey: AttributeKey(stringValue: name)!) func project(_: K.Type) throws { - try K.encode(attributes[K.self]!, to: attributeEncoder) + // We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable + try K.encode(attributes[assumingSendable: K.self]!, to: attributeEncoder) } try project(encodableAttributeType) } // else: the attribute was not in the provided scope or was not encodable, so drop it @@ -336,7 +337,8 @@ extension AttributedString : CodableWithConfiguration { let decodableAttributeType = attributeKeyType as? any DecodableAttributedStringKey.Type { func project(_: K.Type) throws { - attributes[K.self] = try K.decode(from: try attributesContainer.superDecoder(forKey: key)) + // We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable + attributes[assumingSendable: K.self] = try K.decode(from: try attributesContainer.superDecoder(forKey: key)) } try project(decodableAttributeType) } diff --git a/Sources/FoundationEssentials/AttributedString/Conversion.swift b/Sources/FoundationEssentials/AttributedString/Conversion.swift index c486e951a..fad6aba49 100644 --- a/Sources/FoundationEssentials/AttributedString/Conversion.swift +++ b/Sources/FoundationEssentials/AttributedString/Conversion.swift @@ -111,7 +111,8 @@ extension AttributeContainer { for (key, value) in dictionary { if let type = attributeTable[key.rawValue] { func project(_: K.Type) throws { - storage[K.self] = try K._convertFromObjectiveCValue(value as AnyObject) + // We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable + storage[assumingSendable: K.self] = try K._convertFromObjectiveCValue(value as AnyObject) } do { try project(type) @@ -145,7 +146,8 @@ extension Dictionary where Key == NSAttributedString.Key, Value == Any { for key in container.storage.keys { if let type = attributeTable[key] { func project(_: K.Type) throws { - self[NSAttributedString.Key(rawValue: key)] = try K._convertToObjectiveCValue(container.storage[K.self]!) + // We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable + self[NSAttributedString.Key(rawValue: key)] = try K._convertToObjectiveCValue(container.storage[assumingSendable: K.self]!) } do { try project(type)