diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5a877bb..da19e5e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,11 +12,11 @@ concurrency: cancel-in-progress: true env: - XCODE_VERSION: "16.3" + XCODE_VERSION: "26.1" jobs: prepare: - runs-on: macos-15 + runs-on: macos-26 outputs: platforms: ${{ steps.platforms.outputs.platforms }} scheme: ${{ steps.scheme.outputs.scheme }} @@ -62,7 +62,7 @@ jobs: build-and-test: needs: prepare - runs-on: macos-15 + runs-on: macos-26 strategy: fail-fast: false matrix: @@ -81,16 +81,16 @@ jobs: destination="platform=macOS,variant=Mac Catalyst" ;; ios) - destination="platform=iOS Simulator,name=iPhone 16 Pro Max,OS=latest" + destination="platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.1" ;; tvos) - destination="platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=latest" + destination="platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.1" ;; watchos) - destination="platform=watchOS Simulator,name=Apple Watch Series 10 (46mm),OS=latest" + destination="platform=watchOS Simulator,name=Apple Watch Series 11 (46mm),OS=26.1" ;; visionos) - destination="platform=visionOS Simulator,name=Apple Vision Pro,OS=latest" + destination="platform=visionOS Simulator,name=Apple Vision Pro,OS=26.1" ;; *) echo "Unknown platform: ${{ matrix.platform }}" @@ -136,4 +136,4 @@ jobs: if: ${{ matrix.platform == 'macos' }} uses: codecov/codecov-action@v5 with: - fail_ci_if_error: true \ No newline at end of file + fail_ci_if_error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d109093..5431cec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,11 +6,11 @@ on: - '*' env: - XCODE_VERSION: "16.3" + XCODE_VERSION: "26.1" jobs: release: - runs-on: macos-15 + runs-on: macos-26 steps: - uses: actions/checkout@v4 with: diff --git a/.mise.toml b/.mise.toml index 513f2cf..7811bca 100644 --- a/.mise.toml +++ b/.mise.toml @@ -5,8 +5,8 @@ swiftlint = '~/.local/bin/mise x -- swiftlint' swiftformat = '~/.local/bin/mise x -- swiftformat' [tools] -swiftlint = "0.58.2" -swiftformat = "0.55.5" +swiftlint = "0.62.2" +swiftformat = "0.58.5" [tasks.lint] description = 'Run all linters' diff --git a/.swift-version b/.swift-version index e8f1734..913671c 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -6.1 \ No newline at end of file +6.2 \ No newline at end of file diff --git a/.swiftformat b/.swiftformat index 25c4fbb..85fe404 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,106 +1,125 @@ --acronyms ID,URL,UUID --allman false ---anonymousforeach convert ---assetliterals visual-width ---asynccapturing ---beforemarks ---binarygrouping 4,8 ---callsiteparen default ---categorymark "MARK: %c" ---classthreshold 0 ---closingparen default ---closurevoid remove ---commas inline ---complexattrs prev-line ---computedvarattrs prev-line ---condassignment always ---conflictmarkers reject ---dateformat system ---decimalgrouping 3,6 ---doccomments before-declarations ---elseposition same-line ---emptybraces no-space ---enumnamespaces always ---enumthreshold 0 ---exponentcase lowercase ---exponentgrouping disabled ---extensionacl on-declarations ---extensionlength 0 ---extensionmark "MARK: - %t + %c" ---fractiongrouping disabled +--allow-partial-wrapping true +--anonymous-for-each convert +--asset-literals visual-width +--async-capturing +--before-marks +--binary-grouping 4,8 +--blank-line-after-switch-case multiline-only +--call-site-paren default +--category-mark "MARK: %c" +--class-threshold 0 +--closing-paren default +--closure-void remove +--complex-attributes prev-line +--computed-var-attributes prev-line +--conditional-assignment always +--conflict-markers reject +--date-format system +--decimal-grouping 3,6 +--default-test-suite-attributes +--doc-comments before-declarations +--else-position same-line +--empty-braces no-space +--enum-namespaces always +--enum-threshold 0 +--equatable-macro none +--exponent-case lowercase +--exponent-grouping disabled +--extension-acl on-declarations +--extension-mark "MARK: - %t + %c" +--extension-threshold 0 +--file-macro "#file" +--fraction-grouping disabled --fragment false ---funcattributes prev-line ---generictypes ---groupblanklines true ---groupedextension "MARK: %c" ---guardelse next-line +--func-attributes prev-line +--generic-types +--group-blank-lines true +--grouped-extension "MARK: %c" +--guard-else next-line --header ignore ---hexgrouping 4,8 ---hexliteralcase uppercase +--hex-grouping 4,8 +--hex-literal-case uppercase --ifdef indent ---importgrouping testable-first +--import-grouping testable-first --indent 4 ---indentcase false ---indentstrings false ---inferredtypes always ---initcodernil false ---inlinedforeach ignore +--indent-case false +--indent-strings false +--inferred-types always +--init-coder-nil false --lifecycle ---lineaftermarks true +--line-after-marks true +--line-between-guards false --linebreaks lf ---markcategories true ---markextensions always ---marktypes always ---maxwidth none ---modifierorder ---nevertrailing ---nilinit remove ---noncomplexattrs ---nospaceoperators ---nowrapoperators ---octalgrouping 4,8 ---operatorfunc spaced ---organizationmode visibility ---organizetypes actor,class,enum,struct ---patternlet hoist ---preservedecls ---preservedsymbols Package ---propertytypes inferred +--mark-categories true +--mark-class-threshold 0 +--mark-enum-threshold 0 +--mark-extension-threshold 0 +--mark-extensions always +--mark-struct-threshold 0 +--mark-types always +--markdown-files ignore +--max-width none +--modifier-order +--never-trailing +--nil-init remove +--no-space-operators +--no-wrap-operators +--non-complex-attributes +--octal-grouping 4,8 +--operator-func spaced +--organization-mode visibility +--organize-types actor,class,enum,struct +--pattern-let hoist +--preserve-acronyms +--preserve-decls +--preserved-property-types Package +--property-types inferred --ranges spaced +--redundant-async always +--redundant-throws always --self init-only ---selfrequired ---semicolons inline ---shortoptionals always ---smarttabs enabled ---someany true ---sortedpatterns ---storedvarattrs prev-line ---stripunusedargs always ---structthreshold 0 ---tabwidth unspecified ---throwcapturing +--self-required +--semicolons inline-only +--short-optionals always +--single-line-for-each ignore +--smart-tabs enabled +--some-any true +--sort-swiftui-properties none +--sorted-patterns +--stored-var-attributes prev-line +--strip-unused-args always +--struct-threshold 0 +--tab-width unspecified +--throw-capturing --timezone system ---trailingclosures ---trimwhitespace always ---typeattributes prev-line ---typeblanklines preserve ---typedelimiter space-after ---typemark "MARK: - %t" ---typemarks ---typeorder ---visibilitymarks ---visibilityorder ---voidtype void ---wraparguments before-first ---wrapcollections before-first ---wrapconditions after-first ---wrapeffects preserve ---wrapenumcases always ---wrapparameters before-first ---wrapreturntype preserve ---wrapternary before-operators ---wraptypealiases after-first ---xcodeindentation enabled ---yodaswap always ---disable enumNamespaces,fileHeader,headerFileName,redundantInternal,wrap,wrapMultilineStatementBraces,wrapSingleLineComments ---enable acronyms,blankLinesBetweenImports,blockComments,docComments,isEmpty,propertyTypes,redundantProperty,sortSwitchCases,unusedPrivateDeclarations,wrapConditionalBodies,wrapEnumCases +--trailing-closures +--trailing-commas never +--trim-whitespace always +--type-attributes prev-line +--type-blank-lines preserve +--type-body-marks preserve +--type-delimiter space-after +--type-mark "MARK: - %t" +--type-marks +--type-order +--url-macro none +--visibility-marks +--visibility-order +--void-type Void +--wrap-arguments before-first +--wrap-collections before-first +--wrap-conditions after-first +--wrap-effects preserve +--wrap-enum-cases always +--wrap-parameters before-first +--wrap-return-type preserve +--wrap-string-interpolation default +--wrap-ternary before-operators +--wrap-type-aliases after-first +--xcode-indentation enabled +--xctest-symbols +--yoda-swap always +--disable fileHeader,headerFileName,redundantInternal,wrap,wrapMultilineStatementBraces,wrapSingleLineComments +--enable acronyms,blankLinesBetweenImports,blockComments,docComments,emptyExtensions,environmentEntry,isEmpty,noForceTryInTests,noForceUnwrapInTests,noGuardInTests,propertyTypes,redundantAsync,redundantMemberwiseInit,redundantProperty,redundantThrows,singlePropertyPerLine,sortSwitchCases,unusedPrivateDeclarations,wrapConditionalBodies,wrapEnumCases,wrapMultilineFunctionChains diff --git a/.swiftlint.yml b/.swiftlint.yml index 3bb9860..39f5787 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -7,7 +7,7 @@ opt_in_rules: - accessibility_trait_for_button - anonymous_argument_in_multiline_closure - array_init - # async_without_await - not recognized + - async_without_await # attributes # balanced_xctest_lifecycle # closure_body_length @@ -21,7 +21,7 @@ opt_in_rules: - contains_over_first_not_nil - contains_over_range_nil_comparison # contrasted_opening_brace - # convenience_type - not working with Testing framework + # convenience_type - direct_return - discarded_notification_center_observer - discouraged_assert @@ -56,6 +56,7 @@ opt_in_rules: - identical_operands - implicit_return # implicitly_unwrapped_optional + # incompatible_concurrency_annotation # indentation_width - joined_default_parameter - last_where @@ -66,7 +67,7 @@ opt_in_rules: - local_doc_comment - lower_acl_than_parent # missing_docs - # modifier_order + - modifier_order - multiline_arguments - multiline_arguments_brackets - multiline_function_chains @@ -90,6 +91,8 @@ opt_in_rules: - override_in_extension - pattern_matching_keywords - period_spacing + - prefer_asset_symbols + - prefer_condition_list - prefer_key_path # prefer_nimble - prefer_self_in_static_references @@ -123,7 +126,7 @@ opt_in_rules: - static_operator # strict_fileprivate - strong_iboutlet - - superfluous_else + # superfluous_else - switch_case_on_newline - test_case_accessibility - toggle_bool @@ -164,7 +167,7 @@ file_length: warning: 500 identifier_name: - excluded: [id, x, y, z] + excluded: [id, ui, x, y, z, dx, dy, dz] line_length: ignores_comments: true @@ -172,6 +175,9 @@ line_length: nesting: type_level: 2 +no_magic_numbers: + allowed_numbers: [0.0, 1.0, 2.0, 100.0] + type_name: allowed_symbols: ["_"] max_length: 50 @@ -183,11 +189,11 @@ custom_rules: global_actor_attribute_order: name: "Global actor attribute order" message: "Global actor should be the first attribute." - regex: "(?-s)(@.+[^,\\s]\\s+@.*Actor)" + regex: "(?-s)(@.+[^,\\s]\\s+@.*Actor\\s)" sendable_attribute_order: name: "Sendable attribute order" message: "Sendable should be the first attribute." - regex: "(?-s)(@.+[^,\\s]\\s+@Sendable)" + regex: "(?-s)(@.+[^,\\s]\\s+@Sendable\\s)" autoclosure_attribute_order: name: "Autoclosure attribute order" message: "Autoclosure should be the last attribute." diff --git a/Package.swift b/Package.swift index 6936031..e09852f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -57,6 +57,8 @@ let package = Package( for target in package.targets { target.swiftSettings = (target.swiftSettings ?? []) + [ .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny") + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault") ] } diff --git a/README.md b/README.md index d8dc9d4..2552fd7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Principle -![Swift](https://img.shields.io/badge/Swift-6.0-EF5239?logo=swift&labelColor=white) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FNSFatalError%2FPrinciple%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/NSFatalError/Principle) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FNSFatalError%2FPrinciple%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/NSFatalError/Principle) [![Codecov](https://codecov.io/gh/NSFatalError/Principle/graph/badge.svg?token=ITK16CK7NL)](https://codecov.io/gh/NSFatalError/Principle) Essential tools that extend the capabilities of Swift Standard Library. @@ -8,13 +9,16 @@ Essential tools that extend the capabilities of Swift Standard Library. #### Contents - [PrincipleConcurrency](#principleconcurrency) - [PrincipleCollections](#principlecollections) +- [Documentation](#documentation) - [Installation](#installation) ## PrincipleConcurrency `PrincipleConcurrency` introduces `SingleUseTransfer` - an important utility that allows to safely capture `sending` values in closures where the compiler would otherwise prohibit it. -Since Swift currently lacks a built-in annotation to indicate that a closure is guaranteed to be invoked at most once, the compiler may reject code that programmers can prove to be safe. `SingleUseTransfer` shifts the responsibility of ensuring single invocation to the developer while preserving all the benefits of strict concurrency checking — without resorting to tempting workarounds like `@unchecked` or `nonisolated(unsafe)`: +Since Swift currently lacks a built-in annotation to indicate that a closure is guaranteed to be invoked at most once, the compiler may reject code that programmers can prove to be safe. +`SingleUseTransfer` shifts the responsibility of ensuring single invocation to the developer while preserving all the benefits of strict concurrency checking - without resorting +to tempting workarounds like `@unchecked` or `nonisolated(unsafe)`: ```swift let mutex = Mutex(NonSendable()) @@ -54,6 +58,10 @@ var people: [Person] = [...] people.sort(on: \.age) ``` +## Documentation + +[Full documentation is available on the Swift Package Index.](https://swiftpackageindex.com/NSFatalError/Principle/documentation/principle) + ## Installation ```swift diff --git a/Sources/PrincipleConcurrency/SingleUseTransfer.swift b/Sources/PrincipleConcurrency/SingleUseTransfer.swift index ce8dd42..a3921f6 100644 --- a/Sources/PrincipleConcurrency/SingleUseTransfer.swift +++ b/Sources/PrincipleConcurrency/SingleUseTransfer.swift @@ -23,6 +23,7 @@ /// let mutex = Mutex(NonSendable()) /// let instance = NonSendable() /// var transfer = SingleUseTransfer(instance) +/// /// mutex.withLock { protected in /// protected = transfer.finalize() /// } diff --git a/Tests/PrincipleCollectionsTests/SequenceSortedOnTests.swift b/Tests/PrincipleCollectionsTests/SequenceSortedOnTests.swift index 4916c7a..bc01aed 100644 --- a/Tests/PrincipleCollectionsTests/SequenceSortedOnTests.swift +++ b/Tests/PrincipleCollectionsTests/SequenceSortedOnTests.swift @@ -18,26 +18,26 @@ internal struct SequenceSortedOnTests { } @Test - func testSequence() { + func sequence() { let sorted = shuffled.sorted(on: \.value) #expect(Array(range) == sorted.map(\.value)) } @Test - func testReversedSequence() { + func reversedSequence() { let sorted = shuffled.sorted(on: \.value, by: >) #expect(Array(range).reversed() == sorted.map(\.value)) } @Test - func testMutableCollection() { + func mutableCollection() { var sorted = shuffled sorted.sort(on: \.value) #expect(Array(range) == sorted.map(\.value)) } @Test - func testReversedMutableCollection() { + func reversedMutableCollection() { var sorted = shuffled sorted.sort(on: \.value, by: >) #expect(Array(range).reversed() == sorted.map(\.value)) diff --git a/Tests/PrincipleCollectionsTests/StringProtocolCapitalizationTests.swift b/Tests/PrincipleCollectionsTests/StringProtocolCapitalizationTests.swift index 43e571e..8d19346 100644 --- a/Tests/PrincipleCollectionsTests/StringProtocolCapitalizationTests.swift +++ b/Tests/PrincipleCollectionsTests/StringProtocolCapitalizationTests.swift @@ -21,14 +21,14 @@ internal struct StringProtocolCapitalizationTests { private let string = "istanbul city" @Test(arguments: arguments) - func testStringProtocol(locale: Locale?, expectation: String) { + func stringProtocol(locale: Locale?, expectation: String) { let substring = string[...] let transformed = substring.uppercasingFirstCharacter(with: locale) #expect(transformed == expectation) } @Test(arguments: arguments) - func testMutableString(locale: Locale?, expectation: String) { + func mutableString(locale: Locale?, expectation: String) { var transformed = string transformed.uppercaseFirstCharacter(with: locale) #expect(transformed == expectation) diff --git a/Tests/PrincipleConcurrencyTests/TaskTimeLimitTests.swift b/Tests/PrincipleConcurrencyTests/TaskTimeLimitTests.swift index 9f1f974..fc0941e 100644 --- a/Tests/PrincipleConcurrencyTests/TaskTimeLimitTests.swift +++ b/Tests/PrincipleConcurrencyTests/TaskTimeLimitTests.swift @@ -9,12 +9,12 @@ @testable import PrincipleConcurrency import Testing -internal struct TaskTimeLimitTests { +internal enum TaskTimeLimitTests { struct Deadline { @Test - func testSuccessfulOperation() async throws { + func successfulOperation() async throws { let result = try await withDeadline(until: .now + .seconds(1)) { try await Task.sleep(for: .microseconds(1)) return true @@ -23,7 +23,7 @@ internal struct TaskTimeLimitTests { } @Test - func testThrowingOperation() async { + func throwingOperation() async { await #expect(throws: CustomError.self) { try await withDeadline(until: .now + .seconds(1)) { try await Task.sleep(for: .microseconds(1)) @@ -33,7 +33,7 @@ internal struct TaskTimeLimitTests { } @Test - func testExpiredOperation() async { + func expiredOperation() async { await #expect(throws: DeadlineExceededError.self) { try await withDeadline(until: .now + .microseconds(1)) { try await Task.sleep(for: .seconds(1)) @@ -42,7 +42,7 @@ internal struct TaskTimeLimitTests { } @Test - func testCancelledOperation() async { + func cancelledOperation() async { await #expect(throws: CancellationError.self) { let task = Task { try await withDeadline(until: .now + .seconds(1)) { @@ -56,7 +56,7 @@ internal struct TaskTimeLimitTests { } @Test - func testIsolation() async throws { + func isolation() async throws { let task = Task { @CustomActor in try await withDeadline(until: .now + .seconds(1)) { CustomActor.shared.assertIsolated() @@ -69,7 +69,7 @@ internal struct TaskTimeLimitTests { struct Timeout { @Test - func testSuccessfulOperation() async throws { + func successfulOperation() async throws { let result = try await withTimeout(.seconds(1)) { try await Task.sleep(for: .microseconds(1)) return true @@ -78,7 +78,7 @@ internal struct TaskTimeLimitTests { } @Test - func testThrowingOperation() async { + func throwingOperation() async { await #expect(throws: CustomError.self) { try await withTimeout(.seconds(1)) { try await Task.sleep(for: .microseconds(1)) @@ -88,7 +88,7 @@ internal struct TaskTimeLimitTests { } @Test - func testTimedOutOperation() async { + func timedOutOperation() async { await #expect(throws: TimeoutError.self) { try await withTimeout(.microseconds(1)) { try await Task.sleep(for: .seconds(1)) @@ -97,7 +97,7 @@ internal struct TaskTimeLimitTests { } @Test - func testCancelledOperation() async { + func cancelledOperation() async { await #expect(throws: CancellationError.self) { let task = Task { try await withTimeout(.seconds(1)) { @@ -111,7 +111,7 @@ internal struct TaskTimeLimitTests { } @Test - func testIsolation() async throws { + func isolation() async throws { let task = Task { @CustomActor in try await withTimeout(.seconds(1)) { CustomActor.shared.assertIsolated()