From 7191549c8865a3608d47c7949ff8fd8a0ab64dec Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 5 Mar 2024 13:20:13 -0600 Subject: [PATCH] Fix for `@Option(transform:)` with optional type (#619) Due to the restructuring in #477, there was ambiguity between the unconstrained `@Option` initializer that uses a transform (but no initial value) and the one that is constrained to the property being optional. This marks the unconstrained version as disfavored, which allows overload resolution to select the optional version when appropriate. Also fixes this for `@Argument` and improves documentation consistency for `@Option`. Fixes #618. --- .../Documentation.docc/Extensions/Option.md | 6 +- .../Parsable Properties/Argument.swift | 1 + .../Parsable Properties/Option.swift | 265 +++++++++++------- .../OptionalEndToEndTests.swift | 26 ++ 4 files changed, 200 insertions(+), 98 deletions(-) diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/Option.md b/Sources/ArgumentParser/Documentation.docc/Extensions/Option.md index f5a2c9a70..ebb1721d1 100644 --- a/Sources/ArgumentParser/Documentation.docc/Extensions/Option.md +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/Option.md @@ -2,15 +2,15 @@ ## Topics -### Single Options +### Single-Value Options -- ``init(name:parsing:help:completion:)-7slrf`` - ``init(name:parsing:help:completion:)-4yske`` +- ``init(name:parsing:help:completion:)-7slrf`` +- ``init(wrappedValue:name:parsing:help:completion:)-7ilku`` - ``init(name:parsing:help:completion:transform:)-2wf44`` - ``init(name:parsing:help:completion:transform:)-25g7b`` - ``init(wrappedValue:name:parsing:help:completion:transform:)-2llve`` - ``SingleValueParsingStrategy`` -- ``init(wrappedValue:name:parsing:help:completion:)-7ilku`` ### Array Options diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index b9ad507d5..84a9d0cae 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -414,6 +414,7 @@ extension Argument { /// - transform: A closure that converts a string into this property's /// element type or throws an error. @preconcurrency + @_disfavoredOverload public init( help: ArgumentHelp? = nil, completion: CompletionKind? = nil, diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index 27a23f359..9dad1e428 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -31,7 +31,7 @@ /// /// mutating func run() { /// print("\(greeting) \(name)!") -/// if let age = age { +/// if let age { /// print("Congrats on making it to the ripe old age of \(age)!") /// } /// } @@ -226,11 +226,11 @@ public struct ArrayParsingStrategy: Hashable { /// through the terminator `--`. That is the more common approach. For example: /// ```swift /// struct Options: ParsableArguments { - /// @Option var name: String + /// @Option var title: String /// @Argument var remainder: [String] /// } /// ``` - /// would parse the input `--name Foo -- Bar --baz` such that the `remainder` + /// would parse the input `--title Foo -- Bar --baz` such that the `remainder` /// would hold the value `["Bar", "--baz"]`. public static var remaining: ArrayParsingStrategy { self.init(base: .allRemainingInput) @@ -241,20 +241,25 @@ extension ArrayParsingStrategy: Sendable { } // MARK: - @Option T: ExpressibleByArgument Initializers extension Option where Value: ExpressibleByArgument { - /// Creates a property with a default value provided by standard Swift default value syntax. + /// Creates a property with a default value that reads its value from a + /// labeled option. + /// + /// This initializer is used when you declare an `@Option`-attributed property + /// that has an `ExpressibleByArgument` type, providing a default value: /// - /// This method is called to initialize an `Option` with a default value such as: /// ```swift - /// @Option var foo: String = "bar" + /// @Option var title: String = "" /// ``` /// /// - Parameters: /// - wrappedValue: A default value to use for this property, provided - /// implicitly by the compiler during property wrapper initialization. - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. + /// implicitly by the compiler during property wrapper initialization. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. public init( wrappedValue: Value, name: NameSpecification = .long, @@ -294,18 +299,22 @@ extension Option where Value: ExpressibleByArgument { completion: completion) } - /// Creates a property with no default value. + /// Creates a required property that reads its value from a labeled option. + /// + /// This initializer is used when you declare an `@Option`-attributed property + /// that has an `ExpressibleByArgument` type, but without a default value: /// - /// This method is called to initialize an `Option` without a default value such as: /// ```swift - /// @Option var foo: String + /// @Option var title: String /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. public init( name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, @@ -329,22 +338,28 @@ extension Option where Value: ExpressibleByArgument { // MARK: - @Option T Initializers extension Option { - /// Creates a property with a default value provided by standard Swift default value syntax. + /// Creates a property with a default value that reads its value from a + /// labeled option, parsing with the given closure. + /// + /// This initializer is used when you declare an `@Option`-attributed property + /// with a transform closure and a default value: /// - /// This method is called to initialize an `Option` with a default value such as: /// ```swift - /// @Option var foo: String = "bar" + /// @Option(transform: { $0.first ?? " " }) + /// var char: Character = "_" /// ``` /// /// - Parameters: - /// - wrappedValue: A default value to use for this property, provided - /// implicitly by the compiler during property wrapper initialization. - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - wrappedValue: The default value to use for this property, provided + /// implicitly by the compiler during property wrapper initialization. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. /// - transform: A closure that converts a string into this property's - /// element type or throws an error. + /// type, or else throws an error. @preconcurrency public init( wrappedValue: Value, @@ -369,19 +384,28 @@ extension Option { }) } - /// Creates a property with no default value. + /// Creates a required property that reads its value from a labeled option, + /// parsing with the given closure. + /// + /// This initializer is used when you declare an `@Option`-attributed property + /// with a transform closure and without a default value: /// - /// This method is called to initialize an `Option` without a default value such as: /// ```swift - /// @Option var foo: String + /// @Option(transform: { $0.first ?? " " }) + /// var char: Character /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. + /// - transform: A closure that converts a string into this property's + /// type, or else throws an error. @preconcurrency + @_disfavoredOverload public init( name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, @@ -407,16 +431,23 @@ extension Option { // MARK: - @Option Optional<T: ExpressibleByArgument> Initializers extension Option { + /// Creates an optional property that reads its value from a labeled option, + /// with an explicit `nil` default. + /// /// This initializer allows a user to provide a `nil` default value for an - /// optional `@Option`-marked property without allowing a non-`nil` default - /// value. + /// optional `@Option`-marked property: + /// + /// ```swift + /// @Option var count: Int? = nil + /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. + /// - name: A specification for what names are allowed for this option. /// - parsingStrategy: The behavior to use when looking for this option's /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. public init<T>( wrappedValue _value: _OptionalNilComparisonType, name: NameSpecification = .long, @@ -463,18 +494,22 @@ extension Option { }) } - /// Creates a property that reads its value from a labeled option. + /// Creates an optional property that reads its value from a labeled option. /// - /// If the property has an `Optional` type, or you provide a non-`nil` - /// value for the `initial` parameter, specifying this option is not - /// required. + /// This initializer is used when you declare an `@Option`-attributed property + /// with an optional type and no default value: + /// + /// ```swift + /// @Option var count: Int? + /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. + /// - name: A specification for what names are allowed for this option. /// - parsingStrategy: The behavior to use when looking for this option's /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. public init<T>( name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, @@ -498,24 +533,28 @@ extension Option { // MARK: - @Option Optional<T> Initializers extension Option { - /// Creates a property with a default value provided by standard Swift default - /// value syntax, parsing with the given closure. + /// Creates an optional property that reads its value from a labeled option, + /// parsing with the given closure, with an explicit `nil` default. + /// + /// This initializer is used when you declare an `@Option`-attributed property + /// with a transform closure and with a default value of `nil`: /// - /// This method is called to initialize an `Option` with a default value such as: /// ```swift - /// @Option(transform: baz) - /// var foo: String = "bar" + /// @Option(transform: { $0.first ?? " " }) + /// var char: Character? = nil /// ``` /// /// - Parameters: /// - wrappedValue: A default value to use for this property, provided - /// implicitly by the compiler during property wrapper initialization. - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. + /// implicitly by the compiler during property wrapper initialization. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. - /// - transform: A closure that converts a string into this property's type - /// or throws an error. + /// - completion: The type of command-line completion provided for this + /// option. + /// - transform: A closure that converts a string into this property's + /// type, or else throws an error. @preconcurrency public init<T>( wrappedValue _value: _OptionalNilComparisonType, @@ -568,20 +607,26 @@ extension Option { }) } - /// Creates a property with no default value, parsing with the given closure. + /// Creates an optional property that reads its value from a labeled option, + /// parsing with the given closure. + /// + /// This initializer is used when you declare an `@Option`-attributed property + /// with a transform closure and without a default value: /// - /// This method is called to initialize an `Option` with no default value such as: /// ```swift - /// @Option(transform: baz) - /// var foo: String + /// @Option(transform: { $0.first ?? " " }) + /// var char: Character? /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. - /// - transform: A closure that converts a string into this property's type or throws an error. + /// - completion: The type of command-line completion provided for this + /// option. + /// - transform: A closure that converts a string into this property's + /// type, or else throws an error. @preconcurrency public init<T>( name: NameSpecification = .long, @@ -608,16 +653,30 @@ extension Option { // MARK: - @Option Array<T: ExpressibleByArgument> Initializers extension Option { - /// Creates an array property that reads its values from zero or more - /// labeled options. + /// Creates an array property that reads its values from zero or + /// more labeled options. + /// + /// This initializer is used when you declare an `@Option`-attributed array + /// property with a default value: + /// + /// ```swift + /// @Option(name: .customLong("char")) + /// var chars: [Character] = [] + /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - initial: A default value to use for this property. - /// - parsingStrategy: The behavior to use when parsing multiple values - /// from the command-line arguments. + /// - wrappedValue: A default value to use for this property, provided + /// implicitly by the compiler during property wrapper initialization. + /// If this initial value is non-empty, elements passed from the command + /// line are appended to the original contents. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when parsing the elements for + /// this option. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. + /// - transform: A closure that converts a string into this property's + /// element type, or else throws an error. public init<T>( wrappedValue: Array<T>, name: NameSpecification = .long, @@ -639,19 +698,24 @@ extension Option { }) } - /// Creates an array property with no default value that reads its values from zero or more labeled options. + /// Creates a required array property that reads its values from zero or + /// more labeled options. + /// + /// This initializer is used when you declare an `@Option`-attributed array + /// property without a default value: /// - /// This method is called to initialize an array `Option` with no default value such as: /// ```swift - /// @Option() - /// var foo: [String] + /// @Option(name: .customLong("char")) + /// var chars: [Character] /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when parsing the elements for + /// this option. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. public init<T>( name: NameSpecification = .long, parsing parsingStrategy: ArrayParsingStrategy = .singleValue, @@ -675,22 +739,30 @@ extension Option { // MARK: - @Option Array<T> Initializers extension Option { - /// Creates an array property that reads its values from zero or more - /// labeled options, parsing with the given closure. + /// Creates an array property that reads its values from zero or + /// more labeled options, parsing each element with the given closure. /// - /// This property defaults to an empty array if the `initial` parameter - /// is not specified. + /// This initializer is used when you declare an `@Option`-attributed array + /// property with a transform closure and a default value: + /// + /// ```swift + /// @Option(name: .customLong("char"), transform: { $0.first ?? " " }) + /// var chars: [Character] = [] + /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - initial: A default value to use for this property. If `initial` is - /// `nil`, this option defaults to an empty array. - /// - parsingStrategy: The behavior to use when parsing multiple values - /// from the command-line arguments. + /// - wrappedValue: A default value to use for this property, provided + /// implicitly by the compiler during property wrapper initialization. + /// If this initial value is non-empty, elements passed from the command + /// line are appended to the original contents. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when parsing the elements for + /// this option. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. /// - transform: A closure that converts a string into this property's - /// element type or throws an error. + /// element type, or else throws an error. @preconcurrency public init<T>( wrappedValue: Array<T>, @@ -715,23 +787,26 @@ extension Option { }) } - /// Creates an array property with no default value that reads its values from - /// zero or more labeled options, parsing each element with the given closure. + /// Creates a required array property that reads its values from zero or + /// more labeled options, parsing each element with the given closure. + /// + /// This initializer is used when you declare an `@Option`-attributed array + /// property with a transform closure and without a default value: /// - /// This method is called to initialize an array `Option` with no default value such as: /// ```swift - /// @Option(transform: baz) - /// var foo: [String] + /// @Option(name: .customLong("char"), transform: { $0.first ?? " " }) + /// var chars: [Character] /// ``` /// /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when parsing multiple values from - /// the command-line arguments. + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when parsing the elements for + /// this option. /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. + /// - completion: The type of command-line completion provided for this + /// option. /// - transform: A closure that converts a string into this property's - /// element type or throws an error. + /// element type, or else throws an error. @preconcurrency public init<T>( name: NameSpecification = .long, diff --git a/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift index cbb3b830f..9d941554e 100644 --- a/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift @@ -206,3 +206,29 @@ extension OptionalEndToEndTests { XCTAssertThrowsError(try Bar.parse(["-f", "--name", "A"])) } } + +extension OptionalEndToEndTests { + // Compilation test: https://github.com/apple/swift-argument-parser/issues/618 + private struct Command: ParsableCommand { + struct MyError: Error {} + struct Foo { + init?(string: String) { return nil } + } + + @Option(transform: { + guard let foo = Foo(string: $0) else { + throw MyError() + } + return foo + }) + var testOption: Foo? + + @Argument(transform: { + guard let foo = Foo(string: $0) else { + throw MyError() + } + return foo + }) + var testArgument: Foo? + } +}