Skip to content

Commit 511a72a

Browse files
authored
Add Sendable conformance (#582)
This change adds conditional `Sendable` conformance to all property wrapper types when their `Value` is `Sendable`, enabling commands to be used in concurrent contexts. Some notes on the implementation: * Fix flag exclusivity issues This derives the `hasUpdated` check from the parsed values data type, rather than storing it in the closure (which breaks sendability) or passing it through the closure invocation (which wasn't finished enough to actually work). * Mark all `transform` methods as `@Sendable` This allows for a stronger, compiler-supported guarantee of sendability when a compound `ParsableArguments` or `ParsableCommand` type is marked `Sendable`. Most transformations shouldn't be a problem, since the general case is that these are pure string -> value transformations. In cases where making such a transformation sendable is impossible, an author can always change the property to be just a string and perform the transformation within the context of the command's execution, in either the `run()` or `validate()` methods. * Add `@preconcurrency` to Sendable closure APIs This adds the `@preconcurrency` attribute to all public APIs that have changed to take a `@Sendable` closure. This will ease the migration path for sendable adoption for ArgumentParser users, since a warning will only appear for using these APIs (like the `transform` parameter in an @option or @argument) once they've turned on strict concurrency checking. I'm also backing out changes that avoided those warnings in the tests and examples, since in most cases those warnings are spurious; unapplied functions don't capture state. See https://forums.swift.org/t/pitch-inferring-sendable-for-methods-and-key-path-literals/68011 for more on this and hopefully an upcoming fix for these issues. * Raise minimum Swift version to 5.7 In order to provide `@preconcurrency` support, the package needs to have a minimum Swift requirement of 5.7. This makes that change and updates the README to indicate this for the next version.
1 parent 51ec3ed commit 511a72a

File tree

20 files changed

+205
-53
lines changed

20 files changed

+205
-53
lines changed

Package.swift

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.5
1+
// swift-tools-version:5.7
22
//===----------------------------------------------------------*- swift -*-===//
33
//
44
// This source file is part of the Swift Argument Parser open source project
@@ -18,6 +18,9 @@ var package = Package(
1818
.library(
1919
name: "ArgumentParser",
2020
targets: ["ArgumentParser"]),
21+
.plugin(
22+
name: "GenerateManual",
23+
targets: ["GenerateManual"]),
2124
],
2225
dependencies: [],
2326
targets: [
@@ -32,9 +35,18 @@ var package = Package(
3235
exclude: ["CMakeLists.txt"]),
3336
.target(
3437
name: "ArgumentParserToolInfo",
35-
dependencies: [],
38+
dependencies: [ ],
3639
exclude: ["CMakeLists.txt"]),
3740

41+
// Plugins
42+
.plugin(
43+
name: "GenerateManual",
44+
capability: .command(
45+
intent: .custom(
46+
verb: "generate-manual",
47+
description: "Generate a manual entry for a specified target.")),
48+
dependencies: ["generate-manual"]),
49+
3850
// Examples
3951
.executableTarget(
4052
name: "roll",
@@ -49,22 +61,47 @@ var package = Package(
4961
dependencies: ["ArgumentParser"],
5062
path: "Examples/repeat"),
5163

64+
// Tools
65+
.executableTarget(
66+
name: "generate-manual",
67+
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"],
68+
path: "Tools/generate-manual"),
69+
5270
// Tests
5371
.testTarget(
5472
name: "ArgumentParserEndToEndTests",
5573
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
5674
exclude: ["CMakeLists.txt"]),
5775
.testTarget(
58-
name: "ArgumentParserUnitTests",
59-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
60-
exclude: ["CMakeLists.txt"]),
76+
name: "ArgumentParserExampleTests",
77+
dependencies: ["ArgumentParserTestHelpers"],
78+
resources: [.copy("CountLinesTest.txt")]),
79+
.testTarget(
80+
name: "ArgumentParserGenerateManualTests",
81+
dependencies: ["ArgumentParserTestHelpers"]),
6182
.testTarget(
6283
name: "ArgumentParserPackageManagerTests",
6384
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
6485
exclude: ["CMakeLists.txt"]),
6586
.testTarget(
66-
name: "ArgumentParserExampleTests",
67-
dependencies: ["ArgumentParserTestHelpers"],
68-
resources: [.copy("CountLinesTest.txt")]),
87+
name: "ArgumentParserUnitTests",
88+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
89+
exclude: ["CMakeLists.txt"]),
6990
]
7091
)
92+
93+
#if os(macOS)
94+
package.targets.append(contentsOf: [
95+
// Examples
96+
.executableTarget(
97+
name: "count-lines",
98+
dependencies: ["ArgumentParser"],
99+
path: "Examples/count-lines"),
100+
101+
// Tools
102+
.executableTarget(
103+
name: "changelog-authors",
104+
dependencies: ["ArgumentParser"],
105+
path: "Tools/changelog-authors"),
106+
])
107+
#endif
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.6
1+
// swift-tools-version:5.8
22
//===----------------------------------------------------------*- swift -*-===//
33
//
44
// This source file is part of the Swift Argument Parser open source project
@@ -28,7 +28,10 @@ var package = Package(
2828
.target(
2929
name: "ArgumentParser",
3030
dependencies: ["ArgumentParserToolInfo"],
31-
exclude: ["CMakeLists.txt"]),
31+
exclude: ["CMakeLists.txt"],
32+
swiftSettings: [
33+
.enableExperimentalFeature("StrictConcurrency"),
34+
]),
3235
.target(
3336
name: "ArgumentParserTestHelpers",
3437
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"],

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,5 @@ swift-argument-parser | Minimum Swift Version
143143
----------------------|----------------------
144144
`0.0.1 ..< 0.2.0` | 5.1
145145
`0.2.0 ..< 1.1.0` | 5.2
146-
`1.1.0 ...` | 5.5
146+
`1.1.0 ..< 1.3.0` | 5.5
147+
`1.3.0 ...` (future) | 5.7

Sources/ArgumentParser/Parsable Properties/Argument.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ extension Argument: CustomStringConvertible {
9494
}
9595
}
9696

97+
extension Argument: Sendable where Value: Sendable { }
9798
extension Argument: DecodableParsedWrapper where Value: Decodable { }
9899

99100
/// The strategy to use when parsing multiple values from positional arguments
@@ -289,6 +290,8 @@ public struct ArgumentArrayParsingStrategy: Hashable {
289290
}
290291
}
291292

293+
extension ArgumentArrayParsingStrategy: Sendable { }
294+
292295
// MARK: - @Argument T: ExpressibleByArgument Initializers
293296
extension Argument where Value: ExpressibleByArgument {
294297
/// Creates a property with a default value provided by standard Swift default
@@ -373,11 +376,12 @@ extension Argument {
373376
/// - completion: Kind of completion provided to the user for this option.
374377
/// - transform: A closure that converts a string into this property's type
375378
/// or throws an error.
379+
@preconcurrency
376380
public init(
377381
wrappedValue: Value,
378382
help: ArgumentHelp? = nil,
379383
completion: CompletionKind? = nil,
380-
transform: @escaping (String) throws -> Value
384+
transform: @Sendable @escaping (String) throws -> Value
381385
) {
382386
self.init(_parsedValue: .init { key in
383387
let arg = ArgumentDefinition(
@@ -407,10 +411,11 @@ extension Argument {
407411
/// - completion: Kind of completion provided to the user for this option.
408412
/// - transform: A closure that converts a string into this property's
409413
/// element type or throws an error.
414+
@preconcurrency
410415
public init(
411416
help: ArgumentHelp? = nil,
412417
completion: CompletionKind? = nil,
413-
transform: @escaping (String) throws -> Value
418+
transform: @Sendable @escaping (String) throws -> Value
414419
) {
415420
self.init(_parsedValue: .init { key in
416421
let arg = ArgumentDefinition(
@@ -517,11 +522,12 @@ extension Argument {
517522
/// - completion: Kind of completion provided to the user for this option.
518523
/// - transform: A closure that converts a string into this property's
519524
/// element type or throws an error.
525+
@preconcurrency
520526
public init<T>(
521527
wrappedValue _value: _OptionalNilComparisonType,
522528
help: ArgumentHelp? = nil,
523529
completion: CompletionKind? = nil,
524-
transform: @escaping (String) throws -> T
530+
transform: @Sendable @escaping (String) throws -> T
525531
) where Value == Optional<T> {
526532
self.init(_parsedValue: .init { key in
527533
let arg = ArgumentDefinition(
@@ -542,11 +548,12 @@ extension Argument {
542548
Optional @Arguments with default values should be declared as non-Optional.
543549
""")
544550
@_disfavoredOverload
551+
@preconcurrency
545552
public init<T>(
546553
wrappedValue _wrappedValue: Optional<T>,
547554
help: ArgumentHelp? = nil,
548555
completion: CompletionKind? = nil,
549-
transform: @escaping (String) throws -> T
556+
transform: @Sendable @escaping (String) throws -> T
550557
) where Value == Optional<T> {
551558
self.init(_parsedValue: .init { key in
552559
let arg = ArgumentDefinition(
@@ -573,10 +580,11 @@ extension Argument {
573580
/// - completion: Kind of completion provided to the user for this option.
574581
/// - transform: A closure that converts a string into this property's
575582
/// element type or throws an error.
583+
@preconcurrency
576584
public init<T>(
577585
help: ArgumentHelp? = nil,
578586
completion: CompletionKind? = nil,
579-
transform: @escaping (String) throws -> T
587+
transform: @Sendable @escaping (String) throws -> T
580588
) where Value == Optional<T> {
581589
self.init(_parsedValue: .init { key in
582590
let arg = ArgumentDefinition(
@@ -672,12 +680,13 @@ extension Argument {
672680
/// - completion: Kind of completion provided to the user for this option.
673681
/// - transform: A closure that converts a string into this property's
674682
/// element type or throws an error.
683+
@preconcurrency
675684
public init<T>(
676685
wrappedValue: Array<T>,
677686
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
678687
help: ArgumentHelp? = nil,
679688
completion: CompletionKind? = nil,
680-
transform: @escaping (String) throws -> T
689+
transform: @Sendable @escaping (String) throws -> T
681690
) where Value == Array<T> {
682691
self.init(_parsedValue: .init { key in
683692
let arg = ArgumentDefinition(
@@ -711,11 +720,12 @@ extension Argument {
711720
/// - completion: Kind of completion provided to the user for this option.
712721
/// - transform: A closure that converts a string into this property's
713722
/// element type or throws an error.
723+
@preconcurrency
714724
public init<T>(
715725
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
716726
help: ArgumentHelp? = nil,
717727
completion: CompletionKind? = nil,
718-
transform: @escaping (String) throws -> T
728+
transform: @Sendable @escaping (String) throws -> T
719729
) where Value == Array<T> {
720730
self.init(_parsedValue: .init { key in
721731
let arg = ArgumentDefinition(

Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public struct ArgumentHelp {
7878
}
7979
}
8080

81+
extension ArgumentHelp: Sendable { }
82+
8183
extension ArgumentHelp: ExpressibleByStringInterpolation {
8284
public init(stringLiteral value: String) {
8385
self.abstract = value

Sources/ArgumentParser/Parsable Properties/ArgumentVisibility.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public struct ArgumentVisibility: Hashable {
3131
public static let `private` = Self(base: .private)
3232
}
3333

34+
extension ArgumentVisibility: Sendable { }
35+
3436
extension ArgumentVisibility.Representation {
3537
/// A raw Integer value that represents each visibility level.
3638
///

Sources/ArgumentParser/Parsable Properties/CompletionKind.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public struct CompletionKind {
2828
case shellCommand(String)
2929

3030
/// Generate completions using the given closure.
31-
case custom(([String]) -> [String])
31+
case custom(@Sendable ([String]) -> [String])
3232
}
3333

3434
internal var kind: Kind
@@ -59,7 +59,11 @@ public struct CompletionKind {
5959
}
6060

6161
/// Generate completions using the given closure.
62-
public static func custom(_ completion: @escaping ([String]) -> [String]) -> CompletionKind {
62+
@preconcurrency
63+
public static func custom(_ completion: @Sendable @escaping ([String]) -> [String]) -> CompletionKind {
6364
CompletionKind(kind: .custom(completion))
6465
}
6566
}
67+
68+
extension CompletionKind: Sendable { }
69+
extension CompletionKind.Kind: Sendable { }

Sources/ArgumentParser/Parsable Properties/Flag.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ public struct Flag<Value>: Decodable, ParsedWrapper {
109109
}
110110
}
111111

112+
extension Flag: Sendable where Value: Sendable {}
113+
112114
extension Flag: CustomStringConvertible {
113115
public var description: String {
114116
switch _parsedValue {
@@ -156,6 +158,8 @@ public struct FlagInversion: Hashable {
156158
}
157159
}
158160

161+
extension FlagInversion: Sendable { }
162+
159163
/// The options for treating enumeration-based flags as exclusive.
160164
public struct FlagExclusivity: Hashable {
161165
internal enum Representation {
@@ -182,6 +186,8 @@ public struct FlagExclusivity: Hashable {
182186
}
183187
}
184188

189+
extension FlagExclusivity: Sendable { }
190+
185191
extension Flag where Value == Optional<Bool> {
186192
/// Creates a Boolean property that reads its value from the presence of
187193
/// one or more inverted flags.
@@ -388,10 +394,6 @@ extension Flag where Value: EnumerableFlag {
388394
help: ArgumentHelp?
389395
) {
390396
self.init(_parsedValue: .init { key in
391-
// This gets flipped to `true` the first time one of these flags is
392-
// encountered.
393-
var hasUpdated = false
394-
395397
// Create a string representation of the default value. Since this is a
396398
// flag, the default value to show to the user is the `--value-name`
397399
// flag that a user would provide on the command line, not a Swift value.
@@ -434,7 +436,7 @@ extension Flag where Value: EnumerableFlag {
434436
parsingStrategy: .default,
435437
initialValue: initial,
436438
update: .nullary({ (origin, name, values) in
437-
hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
439+
try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, exclusivity: exclusivity)
438440
})
439441
)
440442
}
@@ -511,10 +513,6 @@ extension Flag {
511513
help: ArgumentHelp? = nil
512514
) where Value == Element?, Element: EnumerableFlag {
513515
self.init(_parsedValue: .init { parentKey in
514-
// This gets flipped to `true` the first time one of these flags is
515-
// encountered.
516-
var hasUpdated = false
517-
518516
let caseHelps = Element.allCases.map { Element.help(for: $0) }
519517
let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil })
520518

@@ -532,7 +530,7 @@ extension Flag {
532530
isComposite: !hasCustomCaseHelp)
533531

534532
return ArgumentDefinition.flag(name: name, key: parentKey, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in
535-
hasUpdated = try ArgumentSet.updateFlag(key: parentKey, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
533+
try ArgumentSet.updateFlag(key: parentKey, value: value, origin: origin, values: &values, exclusivity: exclusivity)
536534
}))
537535

538536
}
@@ -621,3 +619,4 @@ extension ArgumentDefinition {
621619
})
622620
}
623621
}
622+

Sources/ArgumentParser/Parsable Properties/NameSpecification.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/// label.
1414
public struct NameSpecification: ExpressibleByArrayLiteral {
1515
/// An individual property name translation.
16-
public struct Element: Hashable {
16+
public struct Element: Hashable, Sendable {
1717
internal enum Representation: Hashable {
1818
case long
1919
case customLong(_ name: String, withSingleDash: Bool)
@@ -80,6 +80,8 @@ public struct NameSpecification: ExpressibleByArrayLiteral {
8080
}
8181
}
8282

83+
extension NameSpecification: Sendable { }
84+
8385
extension NameSpecification {
8486
/// Use the property's name converted to lowercase with words separated by
8587
/// hyphens.

0 commit comments

Comments
 (0)