From 3c8420422e44da42800fc712456f4dde5f14484d Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Wed, 28 May 2025 09:11:42 -0700 Subject: [PATCH 1/6] Address various warnings across the project (#1321) --- .../FoundationInternationalization/Locale/Locale_ICU.swift | 4 ++-- .../FoundationInternationalization/Locale/Locale_ObjC.swift | 3 +++ .../TimeZone/TimeZone_ICU.swift | 6 ++++++ .../AttributedStringIndexValidityTests.swift | 6 +++--- .../AttributedString/AttributedStringTests.swift | 2 +- Tests/FoundationEssentialsTests/DataIOTests.swift | 1 + Tests/FoundationEssentialsTests/StringTests.swift | 2 +- .../DateComponentsTests.swift | 4 ++-- .../Formatting/DateFormatStyleTests.swift | 2 +- .../Formatting/NumberFormatStyleTests.swift | 2 +- 10 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Sources/FoundationInternationalization/Locale/Locale_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale_ICU.swift index c22025ee8..ca9b86e43 100644 --- a/Sources/FoundationInternationalization/Locale/Locale_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale_ICU.swift @@ -1209,8 +1209,8 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { } } - // Check prefs - if let firstWeekdayPref = prefs?.firstWeekday { + // Check prefs. The value doesn't matter here - we check it again in the `forceFirstWeekday` function, and it is immutable. + if prefs?.firstWeekday != nil { let calendarId = calendarIdentifier if let first = forceFirstWeekday(calendarId) { state.firstDayOfWeek = first diff --git a/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift b/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift index ea79592cc..ff2c995b6 100644 --- a/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift +++ b/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift @@ -308,7 +308,10 @@ internal class _NSSwiftLocale: _NSLocaleBridge, @unchecked Sendable { switch locale.temperatureUnit { case .celsius: return NSLocaleTemperatureUnitCelsius case .fahrenheit: return NSLocaleTemperatureUnitFahrenheit +#if !FOUNDATION_FRAMEWORK + // On non-framework builds, the enum is non-closed and `package` visibility, so we need a default default: return NSLocaleTemperatureUnitCelsius +#endif } case .decimalSeparator: return self.decimalSeparator case .groupingSeparator: return self.groupingSeparator diff --git a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift index f1b35e649..931807868 100644 --- a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift +++ b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift @@ -222,6 +222,12 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { return Self.timeZoneDisplayName(for: c, timeZoneName: identifier, localeName: locID, isShort: false, isGeneric: true, isDaylight: false) case .shortGeneric: return Self.timeZoneDisplayName(for: c, timeZoneName: identifier, localeName: locID, isShort: true, isGeneric: true, isDaylight: false) +#if FOUNDATION_FRAMEWORK + // We only need this when building in ObjC mode, when the enum comes from a .h + @unknown default: + // Use standard style + return Self.timeZoneDisplayName(for: c, timeZoneName: identifier, localeName: locID, isShort: false, isGeneric: false, isDaylight: false) +#endif } } } diff --git a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringIndexValidityTests.swift b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringIndexValidityTests.swift index c9a10e600..9237c0fbc 100644 --- a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringIndexValidityTests.swift +++ b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringIndexValidityTests.swift @@ -140,7 +140,7 @@ final class AttributedStringIndexValidityTests: XCTestCase { } public func testMutationInvalidation() { - func checkInPlace(_ mutation: (inout AttributedString) -> (), file: StaticString = #file, line: UInt = #line) { + func checkInPlace(_ mutation: (inout AttributedString) -> (), file: StaticString = #filePath, line: UInt = #line) { var str = AttributedString("Hello World") let idxA = str.startIndex let idxB = str.index(afterCharacter: idxA) @@ -158,7 +158,7 @@ final class AttributedStringIndexValidityTests: XCTestCase { XCTAssertFalse(RangeSet(idxA ..< idxB).isValid(within: str), "Initial range set was valid in in-place mutated", file: file, line: line) } - func checkCopy(_ mutation: (inout AttributedString) -> (), file: StaticString = #file, line: UInt = #line) { + func checkCopy(_ mutation: (inout AttributedString) -> (), file: StaticString = #filePath, line: UInt = #line) { let str = AttributedString("Hello World") let idxA = str.startIndex let idxB = str.index(afterCharacter: idxA) @@ -185,7 +185,7 @@ final class AttributedStringIndexValidityTests: XCTestCase { XCTAssertFalse(RangeSet(idxA ..< idxB).isValid(within: copy), "Initial range set was valid in copy", file: file, line: line) } - func check(_ mutation: (inout AttributedString) -> (), file: StaticString = #file, line: UInt = #line) { + func check(_ mutation: (inout AttributedString) -> (), file: StaticString = #filePath, line: UInt = #line) { checkInPlace(mutation, file: file, line: line) checkCopy(mutation, file: file, line: line) } diff --git a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift index 99df913cc..25a44fe90 100644 --- a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift +++ b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift @@ -1882,7 +1882,7 @@ E { func check( _ a: some Sequence, _ b: some Sequence, - file: StaticString = #file, line: UInt = #line + file: StaticString = #filePath, line: UInt = #line ) { XCTAssertTrue( a.elementsEqual(b), diff --git a/Tests/FoundationEssentialsTests/DataIOTests.swift b/Tests/FoundationEssentialsTests/DataIOTests.swift index 324012c11..fdaca9b56 100644 --- a/Tests/FoundationEssentialsTests/DataIOTests.swift +++ b/Tests/FoundationEssentialsTests/DataIOTests.swift @@ -185,6 +185,7 @@ class DataIOTests : XCTestCase { #if FOUNDATION_FRAMEWORK // String(contentsOf:) is not available outside the framework yet + @available(*, deprecated) func test_emptyFileString() { let data = Data() let url = testURL() diff --git a/Tests/FoundationEssentialsTests/StringTests.swift b/Tests/FoundationEssentialsTests/StringTests.swift index 7b7cb041b..39b5d3672 100644 --- a/Tests/FoundationEssentialsTests/StringTests.swift +++ b/Tests/FoundationEssentialsTests/StringTests.swift @@ -1319,7 +1319,7 @@ final class StringTests : XCTestCase { } - func verifyEncoding(_ encoding: String._Encoding, valid: [String], invalid: [String], file: StaticString = #file, line: UInt = #line) throws { + func verifyEncoding(_ encoding: String._Encoding, valid: [String], invalid: [String], file: StaticString = #filePath, line: UInt = #line) throws { for string in valid { let data = try XCTUnwrap(string.data(using: encoding), "Failed to encode \(string.debugDescription)", file: file, line: line) XCTAssertNotNil(String(data: data, encoding: encoding), "Failed to decode \(data) (\(string.debugDescription))", file: file, line: line) diff --git a/Tests/FoundationInternationalizationTests/DateComponentsTests.swift b/Tests/FoundationInternationalizationTests/DateComponentsTests.swift index e9c9c9242..14456e5e2 100644 --- a/Tests/FoundationInternationalizationTests/DateComponentsTests.swift +++ b/Tests/FoundationInternationalizationTests/DateComponentsTests.swift @@ -141,8 +141,8 @@ final class DateComponentsTests : XCTestCase { extension DateComponentsTests { func date(from string: String, nanoseconds: Int? = nil) -> Date { let d = try! Date(string, strategy: Date.ParseStrategy(format: "\(year: .extended(minimumLength: 4))-\(month: .twoDigits)-\(day: .twoDigits) \(hour: .twoDigits(clock: .twentyFourHour, hourCycle: .zeroBased)):\(minute: .twoDigits):\(second: .twoDigits) \(timeZone: .iso8601(.short))", locale: Locale(identifier: "en_US"), timeZone: TimeZone.gmt)) - if let nanoseconds { - var comps = Calendar(identifier: .gregorian).dateComponents([.era, .year, .month, .day, .hour, .minute, .second, .nanosecond, .weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear, .yearForWeekOfYear, .timeZone, .calendar], from: d) + if nanoseconds != nil { + let comps = Calendar(identifier: .gregorian).dateComponents([.era, .year, .month, .day, .hour, .minute, .second, .nanosecond, .weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear, .yearForWeekOfYear, .timeZone, .calendar], from: d) return Calendar(identifier: .gregorian).date(from: comps)! } return d diff --git a/Tests/FoundationInternationalizationTests/Formatting/DateFormatStyleTests.swift b/Tests/FoundationInternationalizationTests/Formatting/DateFormatStyleTests.swift index c40b59741..807248f67 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/DateFormatStyleTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/DateFormatStyleTests.swift @@ -712,7 +712,7 @@ final class DateAttributedFormatStyleTests : XCTestCase { func test(dateStyle: Date.FormatStyle.DateStyle, timeStyle: Date.FormatStyle.TimeStyle, dateFormatOverride: [Date.FormatStyle.DateStyle: String], expected: [Segment], file: StaticString = #filePath, line: UInt = #line) { let locale = Locale.localeAsIfCurrent(name: enUS, overrides: .init(dateFormats: dateFormatOverride)) - let style = Date.FormatStyle(date: dateStyle, time: timeStyle, locale: locale, calendar: Calendar(identifier: .gregorian), timeZone: TimeZone(identifier: "PST")!, capitalizationContext: .standalone).attributed + let style = Date.FormatStyle(date: dateStyle, time: timeStyle, locale: locale, calendar: Calendar(identifier: .gregorian), timeZone: TimeZone(identifier: "PST")!, capitalizationContext: .standalone).attributedStyle XCTAssertEqual(style.format(date), expected.attributedString, file: file, line: line) } diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift index 652146566..2623c40cf 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift @@ -597,7 +597,7 @@ final class NumberFormatStyleTests: XCTestCase { func testCurrency_Codable() throws { let gbpInUS = Decimal.FormatStyle.Currency(code: "GBP", locale: enUSLocale) - let encoded = try JSONEncoder().encode(gbpInUS) + let _ = try JSONEncoder().encode(gbpInUS) // Valid JSON presentation of the format style let previouslyEncoded = """ { From e072f824bcd6f0d4bdb28142d15439e8afc4df00 Mon Sep 17 00:00:00 2001 From: Tina L <49205802+itingliu@users.noreply.github.com> Date: Wed, 28 May 2025 09:24:39 -0700 Subject: [PATCH 2/6] Initiating a Locale/Language with empty language code/script/region results in an unreasonable locale. (#1102) * Initiating a Locale/Language with empty language code/script/region results in an unreasonable locale. Here are some examples: ```swift print(Locale(languageCode: "", script: "", languageRegion: "").identifier) // "-_" let languageComponents = Locale.Language.Components(language: .init(identifier:"")) print(Locale(languageComponents: languageComponents).identifier) // "-Latn" ``` Fix this by handling empty identifiers passed in at initialization time correctly. resolves 132353443 * Use `isEmpty` check for empty strings --- .../Locale/Locale+Language.swift | 4 +- .../Locale/Locale+Components_ICU.swift | 10 ++ .../LocaleTests.swift | 114 ++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/Locale/Locale+Language.swift b/Sources/FoundationEssentials/Locale/Locale+Language.swift index 69ce38bf1..d793c3260 100644 --- a/Sources/FoundationEssentials/Locale/Locale+Language.swift +++ b/Sources/FoundationEssentials/Locale/Locale+Language.swift @@ -38,11 +38,11 @@ extension Locale { if let languageCode = languageCode { result += languageCode._normalizedIdentifier } - if let script = script { + if let script = script, !script.identifier.isEmpty { result += "-" result += script._normalizedIdentifier } - if let region = region { + if let region = region, !region.identifier.isEmpty { result += "_" result += region._normalizedIdentifier } diff --git a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift index 4fb3bd075..b4cc25488 100644 --- a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift @@ -525,6 +525,11 @@ extension Locale.Language { public var minimalIdentifier : String { let componentsIdentifier = components.identifier + guard !componentsIdentifier.isEmpty else { + // Just return "". Nothing to reduce. + return componentsIdentifier + } + let localeIDWithLikelySubtags = _withFixedCharBuffer { buffer, size, status in return uloc_minimizeSubtags(componentsIdentifier, buffer, size, &status) } @@ -543,6 +548,11 @@ extension Locale.Language { /// Returns a BCP-47 identifier that always includes the script: "zh-Hant-TW", "en-Latn-US" public var maximalIdentifier : String { let id = components.identifier + guard !id.isEmpty else { + // Just return "" instead of trying to fill it up + return id + } + let localeIDWithLikelySubtags = _withFixedCharBuffer { buffer, size, status in return uloc_addLikelySubtags(id, buffer, size, &status) } diff --git a/Tests/FoundationInternationalizationTests/LocaleTests.swift b/Tests/FoundationInternationalizationTests/LocaleTests.swift index c9dbdfa7f..118fd329d 100644 --- a/Tests/FoundationInternationalizationTests/LocaleTests.swift +++ b/Tests/FoundationInternationalizationTests/LocaleTests.swift @@ -165,6 +165,14 @@ final class LocaleTests : XCTestCase { return Locale.Components(identifier: "") } + verify(cldr: "root", bcp47: "und", icu: "") { + return Locale.Components(languageCode: "", script: "", languageRegion: "") + } + + verify(cldr: "root", bcp47: "und", icu: "") { + return Locale.Components(languageCode: nil, script: nil, languageRegion: nil) + } + verify(cldr: "und_US", bcp47: "und-US", icu: "_US") { return Locale.Components(languageRegion: .unitedStates) } @@ -370,6 +378,110 @@ final class LocaleTests : XCTestCase { let result = Locale.identifier(fromWindowsLocaleCode: -1) XCTAssertNil(result) } + + func test_emptyComponents() throws { + + let emptyLocale = Locale(identifier: "") + XCTAssertEqual(emptyLocale.language.languageCode, nil) + XCTAssertEqual(emptyLocale.language.script, nil) + XCTAssertEqual(emptyLocale.language.region, nil) + XCTAssertEqual(emptyLocale.language.maximalIdentifier, "") + XCTAssertEqual(emptyLocale.language.minimalIdentifier, "") + XCTAssertEqual(emptyLocale.identifier, "") + + let localeFromEmptyComp = Locale(components: Locale.Components(identifier: "")) + XCTAssertEqual(localeFromEmptyComp.language.languageCode, nil) + XCTAssertEqual(localeFromEmptyComp.language.script, nil) + XCTAssertEqual(localeFromEmptyComp.language.region, nil) + XCTAssertEqual(localeFromEmptyComp.language.maximalIdentifier, "") + XCTAssertEqual(localeFromEmptyComp.language.minimalIdentifier, "") + XCTAssertEqual(localeFromEmptyComp.identifier, "") + + let localeFromEmptyLanguageComponent = Locale(languageComponents: .init(identifier: "")) + XCTAssertEqual(localeFromEmptyLanguageComponent.language.languageCode, nil) + XCTAssertEqual(localeFromEmptyLanguageComponent.language.script, nil) + XCTAssertEqual(localeFromEmptyLanguageComponent.language.region, nil) + XCTAssertEqual(localeFromEmptyLanguageComponent.language.maximalIdentifier, "") + XCTAssertEqual(localeFromEmptyLanguageComponent.language.minimalIdentifier, "") + XCTAssertEqual(localeFromEmptyLanguageComponent.identifier, "") + + let localeFromEmptyLanguageComponentIndividual = Locale(languageComponents: .init(languageCode: "", script: "", region: "")) + XCTAssertEqual(localeFromEmptyLanguageComponentIndividual.language.languageCode, nil) + XCTAssertEqual(localeFromEmptyLanguageComponentIndividual.language.script, nil) + XCTAssertEqual(localeFromEmptyLanguageComponentIndividual.language.region, nil) + XCTAssertEqual(localeFromEmptyLanguageComponentIndividual.language.maximalIdentifier, "") + XCTAssertEqual(localeFromEmptyLanguageComponentIndividual.language.minimalIdentifier, "") + XCTAssertEqual(localeFromEmptyLanguageComponentIndividual.identifier, "") + + let localeFromEmptyIndividualLanguageComponent = Locale(languageCode: "", script: "", languageRegion: "") + XCTAssertEqual(localeFromEmptyIndividualLanguageComponent.language.languageCode, nil) + XCTAssertEqual(localeFromEmptyIndividualLanguageComponent.language.script, nil) + XCTAssertEqual(localeFromEmptyIndividualLanguageComponent.language.region, nil) + XCTAssertEqual(localeFromEmptyIndividualLanguageComponent.language.maximalIdentifier, "") + XCTAssertEqual(localeFromEmptyIndividualLanguageComponent.language.minimalIdentifier, "") + XCTAssertEqual(localeFromEmptyIndividualLanguageComponent.identifier, "") + + // Locale.Component + let compFromEmptyLocale = Locale.Components(locale: emptyLocale) + XCTAssertEqual(compFromEmptyLocale.languageComponents.languageCode, nil) + XCTAssertEqual(compFromEmptyLocale.languageComponents.script, nil) + XCTAssertEqual(compFromEmptyLocale.languageComponents.region, nil) + + let emptyComp = Locale.Components(identifier: "") + XCTAssertEqual(emptyComp.languageComponents.languageCode, nil) + XCTAssertEqual(emptyComp.languageComponents.script, nil) + XCTAssertEqual(emptyComp.languageComponents.region, nil) + + // Language + let emptyLanguage = Locale.Language(identifier: "") + XCTAssertEqual(emptyLanguage.languageCode, nil) + XCTAssertEqual(emptyLanguage.script, nil) + XCTAssertEqual(emptyLanguage.region, nil) + XCTAssertEqual(emptyLanguage.maximalIdentifier, "") + XCTAssertEqual(emptyLanguage.minimalIdentifier, "") + + let languageFromEmptyComponents = Locale.Language(components: .init(identifier: "")) + XCTAssertEqual(languageFromEmptyComponents.languageCode, nil) + XCTAssertEqual(languageFromEmptyComponents.script, nil) + XCTAssertEqual(languageFromEmptyComponents.region, nil) + XCTAssertEqual(languageFromEmptyComponents.maximalIdentifier, "") + XCTAssertEqual(languageFromEmptyComponents.minimalIdentifier, "") + + let languageFromEmptyComponents2 = Locale.Language(components: .init(languageCode: "", script: "", region: "")) + XCTAssertEqual(languageFromEmptyComponents2.languageCode, "") + XCTAssertEqual(languageFromEmptyComponents2.script, "") + XCTAssertEqual(languageFromEmptyComponents2.region, "") + XCTAssertEqual(languageFromEmptyComponents2.maximalIdentifier, "") + XCTAssertEqual(languageFromEmptyComponents2.minimalIdentifier, "") + + // Language.Component + let languageCompFromEmptyLanguage = Locale.Language.Components(language: Locale.Language(identifier: "")) + XCTAssertEqual(languageCompFromEmptyLanguage.languageCode, nil) + XCTAssertEqual(languageCompFromEmptyLanguage.script, nil) + XCTAssertEqual(languageCompFromEmptyLanguage.region, nil) + + let emptyLanguageComponents = Locale.Language.Components(identifier: "") + XCTAssertEqual(emptyLanguageComponents.languageCode, nil) + XCTAssertEqual(emptyLanguageComponents.script, nil) + XCTAssertEqual(emptyLanguageComponents.region, nil) + + let emptyLanguageComponents2 = Locale.Language.Components(languageCode: "", script: "", region: "") + XCTAssertEqual(emptyLanguageComponents2.languageCode, "") + XCTAssertEqual(emptyLanguageComponents2.script, "") + XCTAssertEqual(emptyLanguageComponents2.region, "") + } + + func test_nilComponents() { + let nilLanguageComponents = Locale.Language.Components(languageCode: nil, script: nil, region: nil) + XCTAssertEqual(nilLanguageComponents.languageCode, nil) + XCTAssertEqual(nilLanguageComponents.script, nil) + XCTAssertEqual(nilLanguageComponents.region, nil) + + let nilLanguage = Locale.Language(languageCode: nil, script: nil, region: nil) + XCTAssertEqual(nilLanguage.languageCode, nil) + XCTAssertEqual(nilLanguage.script, nil) + XCTAssertEqual(nilLanguage.region, nil) + } } final class LocalePropertiesTests : XCTestCase { @@ -417,6 +529,8 @@ final class LocalePropertiesTests : XCTestCase { verify(components: Locale.Components(languageCode: "zh", script: "Hant", languageRegion: "TW"), identifier: "zh_TW") verify(components: Locale.Components(languageCode: "zh", script: "Hans", languageRegion: "TW"), identifier: "zh-Hans_TW") + verify(components: .init(languageCode: "", script: "", languageRegion: ""), identifier: "") + var custom = Locale.Components(languageCode: "en", languageRegion: "US") custom.measurementSystem = .metric custom.currency = "GBP" From d3ff15a5f97ba26324c24a2fa7cd5db111e75329 Mon Sep 17 00:00:00 2001 From: Tina L <49205802+itingliu@users.noreply.github.com> Date: Thu, 29 May 2025 10:05:48 -0700 Subject: [PATCH 3/6] Update evolution.md: add link to Foundation Project (#1309) --- Evolution.md | 54 ++++------------------------------------------------ 1 file changed, 4 insertions(+), 50 deletions(-) diff --git a/Evolution.md b/Evolution.md index 7ee85937c..e5106a716 100644 --- a/Evolution.md +++ b/Evolution.md @@ -9,9 +9,9 @@ A group of core contributors and stakeholders form the _Foundation Workgroup_, w *This section outlines the general process for features with a larger scope. For minor API proposals, please refer to the [Abbreviated review for minor proposals](#abbreviated-review-for-minor-proposals) section.* * **Consider the goal**: Before proposing a change, please consider how your idea fits into the goals and themes for the upcoming release. -* **Socialize the idea**: Propose a rough sketch of the idea in the "pitches" section of the Swift forums, the problems it solves, what the solution looks like, etc., to gauge interest from the community. +* **Socialize the idea**: Propose a rough sketch of the idea on the [Swift forums - Foundation project](https://forums.swift.org/c/related-projects/foundation/99), the problems it solves, what the solution looks like, etc., to gauge interest from the community. Please feel free to include "[Pitch]" in the title. * **Develop the proposal**: Expand the rough sketch into a complete proposal, using the [proposal template](Proposals/0000-template.md), and continue to refine the proposal on the forums. Prototyping an implementation and its uses along with the proposal is required because it helps ensure both technical feasibility of the proposal as well as validating that the proposal solves the problems it is meant to solve. -* **Request a review**: Initiate a pull request to the swift-foundation repository to indicate to the workgroup that you would like the proposal to be reviewed. When the proposal is sufficiently detailed and clear, and addresses feedback from earlier discussions of the idea, the pull request will be accepted. The proposal will be assigned a proposal number as well as a Foundation Workgroup member to manage the review. +* **Request a review**: Initiate a pull request to the swift-foundation repository to indicate to the workgroup that you would like the proposal to be reviewed. When the proposal is sufficiently detailed and clear, and addresses feedback from earlier discussions of the idea, the pull request will be accepted. The proposal will be assigned a proposal number as well as a Foundation Workgroup member to manage the review. This marks as the start of the review. * **Address feedback**: In general, and especially during the review period, be responsive to questions and feedback about the proposal. ### The review process @@ -22,7 +22,7 @@ The review process for a particular proposal begins when a member of the Foundat The review manager will work with the proposal authors to schedule the review. Reviews usually last a single week, but can run longer for particularly large or complex proposals. -When the scheduled review period arrives, the review manager will post the proposal to the Swift forums with the proposal title. To avoid delays, it is important that the proposal authors be available to answer questions, address feedback, and clarify their intent during the review period. +When the scheduled review period arrives, the review manager will post the proposal to the [Swift forums - Foundation Project](https://forums.swift.org/c/related-projects/foundation/99) with the proposal title. To avoid delays, it is important that the proposal authors be available to answer questions, address feedback, and clarify their intent during the review period. After the review has completed, the Foundation Workgroup will make a decision on the proposal. The review manager is responsible for determining consensus among the Foundation Workgroup members, then reporting their decision to the proposal authors and forums. The review manager will update the proposal's state in the repository to reflect that decision. @@ -31,53 +31,7 @@ After the review has completed, the Foundation Workgroup will make a decision on Minor API enhancement ideas that have gained community interest through GitHub issues or forum threads may take a shorter review process. Examples include extending existing types with new functions or variables, or adding new `case` to `enum`. Instead of requiring both a pitch thread and a review, these changes can be proposed directly with a proposal document on a pitch thread. The workgroup has appointed an API champion (currently @itingliu) to oversee this process. Here's what you would do: * **Develop the proposal**: Prepare the proposal using the [proposal template](Proposals/0000-template.md) with a prototype. -* **Request an abbreviated review**: Initiate a pull request to the swift-foundation repository to indicate to the workgroup that you would like the proposal to be reviewed. Meanwhile, post the pull request on the "pitches" section of the Swift forums. Upon seeing the pitch on the forum, a workgroup member will be assigned to manage the review. +* **Request an abbreviated review**: Initiate a pull request to the swift-foundation repository to indicate to the workgroup that you would like the proposal to be reviewed. Meanwhile, post the content on the [Swift forums - Foundation project](https://forums.swift.org/c/related-projects/foundation/99). Upon seeing the pitch on the forum, a workgroup member will be assigned to manage the review. The review manager will comment in the pitch post if the proposal is deemed suitable for abbreviated review, and communicate the next steps on the pitch post. * **Address feedback**: Be responsive to questions and feedback and continue to refine the proposal as needed. At the end of the review period, the review manager will accept the proposal if there is a broad agreement among workgroup members and the community. - - -### Appendix: Review Announcement Template - -(Credit: This section is adapted from [Swift-Evolution's announcement template](https://github.com/apple/swift-evolution/blob/main/process.md#review-announcement)) - -When a proposal enters review, a new topic will be posted to the [Foundation project of the Swift forums](https://forums.swift.org/c/related-projects/foundation/) using the following template. - -
- Template - ---- -Hello Swift community, - -The review of [\<\>]\(\<\>) begins now and runs through \<\> - -Reviews are an important part of the Swift-Foundation evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by \<\>. When contacting the review manager directly, please include proposal name in the subject line. - - -##### Trying it out - -If you'd like to try this proposal out, you can check out \<\>. - -##### What goes into a review? - -The goal of the review process is to improve the proposal under review -through constructive criticism and, eventually, determine the direction of -Swift-Foundation. When writing your review, here are some questions you might want to -answer in your review: - -* What is your evaluation of the proposal? -* Does this proposal fit well with the feel and direction of Swift-Foundation? -* If you have used other languages or libraries with a similar - feature, how do you feel that this proposal compares to those? -* How much effort did you put into your review? A glance, a quick - reading, or an in-depth study? - -More information about Swift-Foundation review process is available at - -> - -Thank you, - --\<\> - -Review Manager From 147607ea9607d3fd19427051a75bb0458936481c Mon Sep 17 00:00:00 2001 From: Tina L <49205802+itingliu@users.noreply.github.com> Date: Thu, 29 May 2025 13:02:38 -0700 Subject: [PATCH 4/6] Skip CI run for any change that only contains changes in markdown file (#1310) * Skip CI run for any change that only contains changes in markdown file * Only exclude markdown on the top level and everything inside 'Proposals/' path --- .github/workflows/pull_request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b634e0ce9..fb871584b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,6 +3,9 @@ name: Pull request on: pull_request: types: [opened, reopened, synchronize] + paths-ignore: + - '*.md' + - 'Proposals/**' jobs: tests: From 5db7965524493b0bf5a4b266d20f64b313338611 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 29 May 2025 14:42:18 -0700 Subject: [PATCH 5/6] Add a fast-path for equality checking when the backing is empty or pointer equal (#1322) * Add a fast-path for equality checking when the backing is empty or pointer-equal * Address review feedback --- .../Essentials/BenchmarkEssentials.swift | 87 +++++++++++++++++++ Sources/FoundationEssentials/Data/Data.swift | 26 +++++- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift index fcc25aab8..fc5ef7e55 100644 --- a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift +++ b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift @@ -34,4 +34,91 @@ let benchmarks = { assert(u1 != u2) } } + + // MARK: Data + + func createSomeData(_ length: Int) -> Data { + var d = Data(repeating: 42, count: length) + // Set a byte to be another value just so we know we have a unique pointer to the backing + // For maximum inefficiency in the not equal case, set the last byte + d[length - 1] = UInt8.random(in: UInt8.min.. TwoDatasBox in + let d1 = Data() + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataEqualInline", closure: { benchmark, box in + blackHole(box.d1 == box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(12) // Less than size of InlineData.Buffer + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataNotEqualInline", closure: { benchmark, box in + blackHole(box.d1 != box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(12) // Less than size of InlineData.Buffer + let d2 = createSomeData(12) + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataEqualLarge", closure: { benchmark, box in + blackHole(box.d1 == box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 8) + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataNotEqualLarge", closure: { benchmark, box in + blackHole(box.d1 != box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 8) + let d2 = createSomeData(1024 * 8) + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataEqualReallyLarge", closure: { benchmark, box in + blackHole(box.d1 == box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 1024 * 8) + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataNotEqualReallyLarge", closure: { benchmark, box in + blackHole(box.d1 != box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 1024 * 8) + let d2 = createSomeData(1024 * 1024 * 8) + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + } diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index 178c7cfa3..9f5bb7245 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -2709,14 +2709,36 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect /// Returns `true` if the two `Data` arguments are equal. @inlinable // This is @inlinable as emission into clients is safe -- the concept of equality on Data will not change. public static func ==(d1 : Data, d2 : Data) -> Bool { + // See if both are empty + switch (d1._representation, d2._representation) { + case (.empty, .empty): + return true + default: + // Continue on to checks below + break + } + let length1 = d1.count - if length1 != d2.count { + let length2 = d2.count + + // Unequal length data can never be equal + guard length1 == length2 else { return false } + if length1 > 0 { return d1.withUnsafeBytes { (b1: UnsafeRawBufferPointer) in return d2.withUnsafeBytes { (b2: UnsafeRawBufferPointer) in - return memcmp(b1.baseAddress!, b2.baseAddress!, b2.count) == 0 + // If they have the same base address and same count, it is equal + let b1Address = b1.baseAddress! + let b2Address = b2.baseAddress! + + guard b1Address != b2Address else { + return true + } + + // Compare the contents + return memcmp(b1Address, b2Address, b2.count) == 0 } } } From cb06baa549e0dd3f3d95ea507ab3dc6264bc2d1e Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Fri, 30 May 2025 09:48:07 -0700 Subject: [PATCH 6/6] Update issue templates (#1323) --- .github/ISSUE_TEMPLATE/bug_report.md | 32 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 17 ++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..859bbcb28 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Sample code to reproduce the behavior. + +```swift +Sample code goes here +``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Configuration (please complete the following information):** + - Swift Version: [e.g. Swift 6.1, `main` branch under development, etc] + - OS: [e.g. iOS, macOS, Linux Distribution, Windows, Android] + - OS Version: [e.g. iOS 18] + +**Regression information:** +If applicable, please list older versions where this issue did not occur. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..e3cc4e8e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Request a new API or other enhancement +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context about the feature request here.