Skip to content

Adopt Swift 6 language mode with strict concurrency checking#832

Open
thuyetngocluong wants to merge 4 commits into
malcommac:masterfrom
thuyetngocluong:feature/swift6-strict-concurrency
Open

Adopt Swift 6 language mode with strict concurrency checking#832
thuyetngocluong wants to merge 4 commits into
malcommac:masterfrom
thuyetngocluong:feature/swift6-strict-concurrency

Conversation

@thuyetngocluong

Copy link
Copy Markdown

Summary

This PR migrates SwiftDate to Swift 6 language mode with strict concurrency (data-race safety) enabled, while keeping the public API source-compatible and the runtime behavior unchanged. The library now builds with 0 errors and 0 warnings in Swift 6 mode, and the full test suite passes — including under ThreadSanitizer.

What changed

Package manifest

  • swift-tools-version: 6.0 + swiftLanguageModes: [.v6] (platforms stay at macOS 10.15 / iOS 13 / watchOS 6 / tvOS 13).
  • Removed the two resources: entries pointing at Formatters/RelativeFormatter/langs and Resources — those directories were deleted together with the RelativeFormatter removal, and under tools 6.0 the missing paths are a hard build error (they were silently warned about under 5.5).
  • SwiftDate.podspec: swift_versions bumped to 6.0.

Concurrency-safe shared state

SwiftDate.defaultRegion and DateFormats.autoFormats were stored mutable globals — a hard error under Swift 6. They are now computed properties backed by an internal OSUnfairLocked<Value: Sendable> box:

  • Uses OSAllocatedUnfairLock on iOS 16 / macOS 13 and later, and falls back to the C os_unfair_lock API (heap-allocated, so the lock has a stable address) on older systems.
  • The public synchronous get/set API is unchanged, so every existing call site — including the many region: Region = SwiftDate.defaultRegion default arguments — keeps compiling as-is.

Sendable conformances

Region (its only stored property is a Calendar), ISOFormatter.Options, ISOParser.Options, DateToStringStyles, StringToDateStyles. The ISOFormatter.Options conformance is what resolves the 13 "static property is not concurrency-safe" errors on its option-set constants.

Newer SDK enum cases

Recent SDKs added Calendar.Component.dayOfYear (iOS 18/macOS 15) and .isLeapMonth/.isRepeatedDay (iOS 26/macOS 26), plus 11 new Calendar.Identifier cases — all of which made existing exhaustive switches warn (@unknown default does not cover cases that are known to the SDK you compile against):

  • Calendar.Component.nsCalendarUnit maps the three new cases to their NSCalendar.Unit counterparts behind availability guards (empty set on older OSes, where those values cannot even be constructed).
  • Date.difference(in:) / differences(in:from:) now return a real value for .dayOfYear on iOS 18/macOS 15+; .isLeapMonth/.isRepeatedDay return nil (they are Boolean metadata — no integer difference exists).
  • Calendar.Identifier.description covers the 11 new identifiers, and the conformance is marked @retroactive.

Dead code removal

The unused @Atomic property wrapper and Bundle.appModule/BundleFinder (orphaned since the RelativeFormatter removal, and itself a Swift 6 violation as a stored mutable static).

Tests

  • New TestConcurrencySafety stress tests hammer defaultRegion/autoFormats with DispatchQueue.concurrentPerform while other threads parse and format through them.
  • Two pre-existing test failures on current toolchains were fixed (both reproduce on master): newer Foundation zero-fills and attaches calendar/timeZone to dateComponents(from:to:) results, breaking whole-value DateComponents equality against 1.hours; and JSONEncoder key order is randomized per process, breaking the raw-JSON string comparison (now uses .sortedKeys).

Verification

  • swift build in Swift 6 mode: 0 errors, 0 warnings (Swift 6.3.3 toolchain)
  • swift test: 71/71 passing, repeatedly
  • swift test --sanitize=thread: clean, including the new concurrency stress tests

Notes for reviewers

  • Consuming the package via SwiftPM now requires a Swift 6.0+ toolchain (Xcode 16+). CocoaPods consumers need Xcode 16+ as well (for @retroactive).
  • OSUnfairLocked makes each get/set individually atomic; compound read-modify-write patterns like SwiftDate.autoFormats.append(...) are not atomic as a whole (this is documented on the property, and matches the previous unsynchronized behavior).

🤖 Generated with Claude Code

Migrate the package to swift-tools-version 6.0 with swiftLanguageModes
[.v6] so it builds cleanly (0 errors, 0 warnings) under Swift 6 strict
data-race safety, preserving the public API and runtime behavior.

- Replace the stored mutable globals SwiftDate.defaultRegion and
  DateFormats.autoFormats with computed properties backed by an internal
  OSUnfairLocked box (OSAllocatedUnfairLock on iOS 16/macOS 13+, C
  os_unfair_lock on older systems), keeping the synchronous get/set API.
- Add Sendable conformances: Region, ISOFormatter.Options,
  ISOParser.Options, DateToStringStyles, StringToDateStyles.
- Remove dead code that violated strict concurrency: the unused @atomic
  property wrapper and Bundle.appModule/BundleFinder (orphaned since the
  RelativeFormatter removal), plus the Package.swift resource entries
  pointing at directories deleted in that same cleanup (hard error under
  tools 6.0).
- Handle the Calendar.Component cases introduced by newer SDKs
  (.dayOfYear, .isLeapMonth, .isRepeatedDay) and the new
  Calendar.Identifier cases in exhaustive switches, with availability
  guards; difference(in:) now returns a real value for .dayOfYear on
  iOS 18/macOS 15+. Mark the Calendar.Identifier CustomStringConvertible
  conformance @retroactive.
- Add TestConcurrencySafety stress tests (concurrent defaultRegion and
  autoFormats access); suite is clean under ThreadSanitizer.
- Fix two tests broken by newer Foundation behavior (dateComponents
  zero-fills and attaches calendar/timeZone; JSONEncoder key order is
  randomized per process - use .sortedKeys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 20, 2026 18:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates SwiftDate to Swift 6 language mode with strict concurrency checking, replacing previously mutable global/shared state with synchronized access while keeping the public API surface largely source-compatible. It also updates several types for Sendable compliance, adjusts logic for newer Foundation/SDK enum cases, and updates tests to be deterministic and to validate concurrency safety.

Changes:

  • Adopt Swift 6 tools/language mode in SwiftPM and CocoaPods, and remove stale SwiftPM resource entries.
  • Make SwiftDate.defaultRegion and DateFormats.autoFormats concurrency-safe via an unfair-lock-backed storage wrapper.
  • Add/adjust Sendable conformances and update code/tests for newer Foundation behaviors and newer SDK enum cases.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Tests/SwiftDateTests/TestRegion.swift Makes JSON encoding deterministic by using .sortedKeys and updates expected JSON string order.
Tests/SwiftDateTests/TestDateInRegion+Components.swift Updates a component comparison to avoid brittle whole-DateComponents equality with newer Foundation.
Tests/SwiftDateTests/TestConcurrencySafety.swift Adds stress tests for concurrent reads/writes of defaultRegion and autoFormats.
SwiftDate.podspec Bumps CocoaPods swift_versions to 6.0.
Sources/SwiftDate/SwiftDate.swift Replaces stored mutable defaultRegion with synchronized computed access; keeps autoFormats passthrough.
Sources/SwiftDate/Supports/Commons.swift Introduces OSUnfairLocked and uses it to synchronize DateFormats.autoFormats; updates Calendar component mapping for new cases.
Sources/SwiftDate/Supports/Calendars.swift Adds @retroactive and expands Calendar.Identifier.description for new identifiers.
Sources/SwiftDate/Formatters/ISOParser.swift Marks ISOParser.Options as Sendable.
Sources/SwiftDate/Formatters/ISOFormatter.swift Marks ISOFormatter.Options as Sendable.
Sources/SwiftDate/Formatters/Formatter+Protocols.swift Marks style enums as Sendable.
Sources/SwiftDate/DateInRegion/Region.swift Marks Region as Sendable.
Sources/SwiftDate/Date/Date+Compare.swift Handles newer Calendar.Component cases in component extraction.
Package.swift Moves to swift-tools 6.0, removes deleted resources, and sets swiftLanguageModes: [.v6].
Comments suppressed due to low confidence (1)

Sources/SwiftDate/Supports/Calendars.swift:79

  • New Calendar.Identifier cases were added to description, but init(_ rawValue:) doesn’t map these strings back to the corresponding identifiers. This breaks round-tripping/decoding for the newly supported identifiers (e.g. "bangla" will fall into default and silently become the default calendar).
		case .vikram:				return "vikram"
		@unknown default:
			fatalError("Unsupported calendar \(self)")
		}
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sources/SwiftDate/Supports/Commons.swift
Comment thread Sources/SwiftDate/Supports/Commons.swift Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread Sources/SwiftDate/Supports/Calendars.swift Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comment on lines +277 to +282
case .isLeapMonth:
if #available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) {
return NSCalendar.Unit.isLeapMonth
} else {
return []
}
Comment on lines +283 to +288
case .isRepeatedDay:
if #available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) {
return NSCalendar.Unit.isRepeatedDay
} else {
return []
}
The Calendar.Identifier cases added by the iOS 26 SDK and the
Calendar.Component/NSCalendar.Unit additions have different SDK floors:
Calendar.Component.isLeapMonth ships since the iOS 17 SDK, .dayOfYear
since iOS 18, while .isRepeatedDay, NSCalendar.Unit.isLeapMonth/
.isRepeatedDay and the 11 regional calendar identifiers exist only in
the iOS 26 SDK. Wrap each usage in the matching #if compiler(...) so
older toolchains neither fail to compile the missing symbols nor emit
exhaustiveness warnings for cases their SDK does have.

Minimum supported toolchain is Xcode 16 (Swift 6.0), imposed by
swift-tools-version 6.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Comment on lines 13 to +23
import Foundation
import os

// MARK: - Lock Protected Value Support

/// An unfair-lock protected mutable box used to expose the library-wide
/// configuration (`SwiftDate.defaultRegion`, `DateFormats.autoFormats`) as
/// concurrency-safe shared state under Swift 6 strict data-race safety.
/// On iOS 16/macOS 13 and later it uses Apple's `OSAllocatedUnfairLock`;
/// on older systems it falls back to the C `os_unfair_lock` API.
internal final class OSUnfairLocked<Value: Sendable>: @unchecked Sendable {
Comment on lines 73 to 75
}

// MARK: - DateFormatter
Comment on lines +35 to +38
let region = SwiftDate.defaultRegion
XCTAssertFalse(region.timeZone.identifier.isEmpty)
_ = DateInRegion(Date(), region: region).toISO()
_ = Region() // fills every attribute from a single defaultRegion snapshot
Comment on lines +77 to 81
#endif
@unknown default:
fatalError("Unsupported calendar \(self)")
fatalError("Unsupported calendar identifier")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants