Skip to content

Commit bafa74a

Browse files
werm098Mike
and
Mike
authored
Fix invalid state error when decoding an unparsed optional value (#290)
Fixes “Internal error. Invalid state while parsing command-line arguments.” that is encountered when an unparsed value is optional. Root causes: - `ParsedArgumentsContainer.decodeNil` returns false for optional values because it only does a `!contains(key)` check. This should instead return nil if the value of the element is nil. - The decoder did not know about unparsed input origins that and would result in unexpected behavior when decoding nil default values. - The `value` of `Mirror.Child` is defined as `Any` but this is confusing because the value could be `Optional<Any>` which is not equal to `nil` even when the `Optional` case is `.none`. Co-authored-by: Mike <[email protected]>
1 parent b936799 commit bafa74a

File tree

10 files changed

+293
-17
lines changed

10 files changed

+293
-17
lines changed

Sources/ArgumentParser/Parsable Types/ParsableArguments.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ extension ArgumentSet {
241241
key: InputKey(rawValue: codingKey),
242242
kind: .default,
243243
parser: { _ in nil },
244-
default: child.value,
244+
default: Mirror.realValue(for: child),
245245
completion: .default)
246246
definition.help.help = .hidden
247247
return ArgumentSet(definition)

Sources/ArgumentParser/Parsing/ArgumentDecoder.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ final class ParsedArgumentsContainer<K>: KeyedDecodingContainerProtocol where K
104104
}
105105

106106
func decodeNil(forKey key: K) throws -> Bool {
107-
return !contains(key)
107+
return element(forKey: key)?.value == nil
108108
}
109109

110110
func decode<T>(_ type: T.Type, forKey key: K) throws -> T where T : Decodable {
@@ -113,7 +113,11 @@ final class ParsedArgumentsContainer<K>: KeyedDecodingContainerProtocol where K
113113
}
114114

115115
func decodeIfPresent<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? where T : Decodable {
116-
let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(key), parsedElement: element(forKey: key))
116+
let parsedElement = element(forKey: key)
117+
if let parsedElement = parsedElement, parsedElement.inputOrigin.isDefaultValue {
118+
return parsedElement.value as? T
119+
}
120+
let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(key), parsedElement: parsedElement)
117121
do {
118122
return try type.init(from: subDecoder)
119123
} catch let error as ParserError {

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,22 +148,20 @@ extension ArgumentSet {
148148

149149
extension ArgumentDefinition {
150150
/// Create a unary / argument that parses using the given closure.
151-
init<A>(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .nextAsValue, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind) {
152-
let initialValueCreator: (InputOrigin, inout ParsedValues) throws -> Void
153-
if let initialValue = initial {
154-
initialValueCreator = { origin, values in
155-
values.set(initialValue, forKey: key, inputOrigin: origin)
156-
}
157-
} else {
158-
initialValueCreator = { _, _ in }
159-
}
160-
151+
init<A>(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .nextAsValue, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind) {
161152
self.init(kind: kind, help: ArgumentDefinition.Help(key: key), completion: completion, parsingStrategy: parsingStrategy, update: .unary({ (origin, name, value, values) in
162153
guard let v = parser(value) else {
163154
throw ParserError.unableToParseValue(origin, name, value, forKey: key)
164155
}
165156
values.set(v, forKey: key, inputOrigin: origin)
166-
}), initial: initialValueCreator)
157+
}), initial: { origin, values in
158+
switch kind {
159+
case .default:
160+
values.set(initial, forKey: key, inputOrigin: InputOrigin(element: .defaultValue))
161+
case .named, .positional:
162+
values.set(initial, forKey: key, inputOrigin: origin)
163+
}
164+
})
167165

168166
help.options.formUnion(ArgumentDefinition.Help.Options(type: type))
169167
help.defaultValue = initial.map { "\($0)" }

Sources/ArgumentParser/Parsing/InputOrigin.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral {
103103
}
104104
}
105105

106+
extension InputOrigin {
107+
var isDefaultValue: Bool {
108+
return _elements.count == 1 && _elements.first == .defaultValue
109+
}
110+
}
111+
106112
extension InputOrigin.Element {
107113
static func < (lhs: Self, rhs: Self) -> Bool {
108114
switch (lhs, rhs) {

Sources/ArgumentParser/Parsing/ParsedValues.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct InputKey: RawRepresentable, Hashable {
2929
struct ParsedValues {
3030
struct Element {
3131
var key: InputKey
32-
var value: Any
32+
var value: Any?
3333
/// Where in the input that this came from.
3434
var inputOrigin: InputOrigin
3535
fileprivate var shouldClearArrayIfParsed = true
@@ -45,7 +45,7 @@ struct ParsedValues {
4545
}
4646

4747
extension ParsedValues {
48-
mutating func set(_ new: Any, forKey key: InputKey, inputOrigin: InputOrigin) {
48+
mutating func set(_ new: Any?, forKey key: InputKey, inputOrigin: InputOrigin) {
4949
set(Element(key: key, value: new, inputOrigin: inputOrigin))
5050
}
5151

Sources/ArgumentParser/Parsing/ParserError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ enum ParserError: Error {
3838

3939
/// These are errors used internally to the parsing, and will not be exposed to the help generation.
4040
enum InternalParseError: Error {
41-
case wrongType(Any, forKey: InputKey)
41+
case wrongType(Any?, forKey: InputKey)
4242
case subcommandNameMismatch
4343
case subcommandLevelMismatch(Int, Int)
4444
case subcommandLevelMissing(Int)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
extension Mirror {
13+
/// Returns the "real" value of a `Mirror.Child` as `Any?`.
14+
///
15+
/// The `value` of `Mirror.Child` is defined as `Any` but this is confusing because the value
16+
/// could be `Optional<Any>` which is not equal to `nil` even when the `Optional` case is `.none`.
17+
///
18+
/// The purpose of this function is to disambiguate when the `value` of a `Mirror.Child` is actaully nil.
19+
static func realValue(for child: Mirror.Child) -> Any? {
20+
if case Optional<Any>.none = child.value {
21+
return nil
22+
} else {
23+
return child.value
24+
}
25+
}
26+
}

Tests/ArgumentParserEndToEndTests/UnparsedValuesEndToEndTest.swift

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,73 @@ extension UnparsedValuesEndToEndTests {
5757
}
5858
}
5959

60+
// MARK: Two value + unparsed optional variable
61+
62+
fileprivate struct Hogeraa: ParsableArguments {
63+
var fullName: String? = "Full Name"
64+
}
65+
66+
fileprivate struct Hogera: ParsableArguments {
67+
@Option() var firstName: String
68+
@Flag() var hasLastName = false
69+
var fullName: String?
70+
mutating func validate() throws {
71+
if hasLastName { fullName = "\(firstName) LastName" }
72+
}
73+
}
74+
75+
fileprivate struct Piyo: ParsableArguments {
76+
@Option() var firstName: String
77+
@Flag() var hasLastName = false
78+
var fullName: String!
79+
mutating func validate() throws {
80+
fullName = firstName + (hasLastName ? " LastName" : "")
81+
}
82+
}
83+
84+
extension UnparsedValuesEndToEndTests {
85+
func testParsing_TwoPlusOptionalUnparsed() throws {
86+
AssertParse(Hogeraa.self, []) { hogeraa in
87+
XCTAssertEqual(hogeraa.fullName, "Full Name")
88+
}
89+
90+
AssertParse(Hogera.self, ["--first-name", "Hogera"]) { hogera in
91+
XCTAssertEqual(hogera.firstName, "Hogera")
92+
XCTAssertFalse(hogera.hasLastName)
93+
XCTAssertNil(hogera.fullName)
94+
}
95+
AssertParse(Hogera.self, ["--first-name", "Hogera", "--has-last-name"]) { hogera in
96+
XCTAssertEqual(hogera.firstName, "Hogera")
97+
XCTAssertTrue(hogera.hasLastName)
98+
XCTAssertEqual(hogera.fullName, "Hogera LastName")
99+
}
100+
101+
AssertParse(Piyo.self, ["--first-name", "Hogera"]) { piyo in
102+
XCTAssertEqual(piyo.firstName, "Hogera")
103+
XCTAssertFalse(piyo.hasLastName)
104+
XCTAssertEqual(piyo.fullName, "Hogera")
105+
}
106+
AssertParse(Piyo.self, ["--first-name", "Hogera", "--has-last-name"]) { piyo in
107+
XCTAssertEqual(piyo.firstName, "Hogera")
108+
XCTAssertTrue(piyo.hasLastName)
109+
XCTAssertEqual(piyo.fullName, "Hogera LastName")
110+
}
111+
}
112+
113+
func testParsing_TwoPlusOptionalUnparsed_Fails() throws {
114+
XCTAssertThrowsError(try Hogeraa.parse(["--full-name"]))
115+
XCTAssertThrowsError(try Hogeraa.parse(["--full-name", "Hogera Piyo"]))
116+
XCTAssertThrowsError(try Hogera.parse([]))
117+
XCTAssertThrowsError(try Hogera.parse(["--first-name"]))
118+
XCTAssertThrowsError(try Hogera.parse(["--first-name", "Hogera", "--full-name"]))
119+
XCTAssertThrowsError(try Hogera.parse(["--first-name", "Hogera", "--full-name", "Hogera Piyo"]))
120+
XCTAssertThrowsError(try Piyo.parse([]))
121+
XCTAssertThrowsError(try Piyo.parse(["--first-name"]))
122+
XCTAssertThrowsError(try Piyo.parse(["--first-name", "Hogera", "--full-name"]))
123+
XCTAssertThrowsError(try Piyo.parse(["--first-name", "Hogera", "--full-name", "Hogera Piyo"]))
124+
}
125+
}
126+
60127
// MARK: Nested unparsed decodable type
61128

62129

@@ -106,3 +173,86 @@ extension UnparsedValuesEndToEndTests {
106173
XCTAssertThrowsError(try Foo.parse(["--one", "aaa"]))
107174
}
108175
}
176+
177+
// MARK: Nested unparsed optional decodable type
178+
179+
fileprivate struct Barr: ParsableCommand {
180+
var baz: Baz? = Baz(name: "Some Name", age: 105)
181+
}
182+
183+
fileprivate struct Bar: ParsableCommand {
184+
@Flag var bar: Bool = false
185+
var baz: Baz?
186+
var bazz: Bazz?
187+
mutating func validate() throws {
188+
if bar {
189+
baz = Baz(name: "Some", age: 100)
190+
bazz = Bazz(name: "Other", age: 101)
191+
}
192+
}
193+
}
194+
195+
fileprivate struct Baz: Decodable {
196+
var name: String?
197+
var age: Int!
198+
}
199+
200+
fileprivate struct Bazz: Decodable {
201+
var name: String?
202+
var age: Int
203+
}
204+
205+
extension UnparsedValuesEndToEndTests {
206+
func testUnparsedNestedOptionalValue() {
207+
AssertParse(Barr.self, []) { barr in
208+
XCTAssertNotNil(barr.baz)
209+
XCTAssertEqual(barr.baz?.age, 105)
210+
XCTAssertEqual(barr.baz?.name, "Some Name")
211+
}
212+
213+
AssertParse(Bar.self, []) { bar in
214+
XCTAssertFalse(bar.bar)
215+
XCTAssertNil(bar.baz)
216+
XCTAssertNil(bar.baz?.age)
217+
XCTAssertNil(bar.baz?.name)
218+
XCTAssertNil(bar.bazz)
219+
XCTAssertNil(bar.bazz?.age)
220+
XCTAssertNil(bar.bazz?.name)
221+
}
222+
223+
AssertParse(Bar.self, ["--bar"]) { bar in
224+
XCTAssertTrue(bar.bar)
225+
XCTAssertNotNil(bar.baz)
226+
XCTAssertEqual(bar.baz?.name, "Some")
227+
XCTAssertEqual(bar.baz?.age, 100)
228+
XCTAssertNotNil(bar.bazz)
229+
XCTAssertEqual(bar.bazz?.name, "Other")
230+
XCTAssertEqual(bar.bazz?.age, 101)
231+
}
232+
}
233+
234+
func testUnparsedNestedOptionalValue_Fails() {
235+
XCTAssertThrowsError(try Bar.parse(["--baz", "xyz"]))
236+
XCTAssertThrowsError(try Bar.parse(["--bazz", "xyz"]))
237+
XCTAssertThrowsError(try Bar.parse(["--name", "None"]))
238+
XCTAssertThrowsError(try Bar.parse(["--age", "123"]))
239+
XCTAssertThrowsError(try Bar.parse(["--bar", "--name", "None"]))
240+
XCTAssertThrowsError(try Bar.parse(["--bar", "--age", "123"]))
241+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz"]))
242+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz"]))
243+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "--name", "None"]))
244+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--name"]))
245+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--name", "None"]))
246+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "--age", "None"]))
247+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--age"]))
248+
XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--age", "None"]))
249+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz"]))
250+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz"]))
251+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "--name", "None"]))
252+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--name"]))
253+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--name", "None"]))
254+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "--age", "None"]))
255+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--age"]))
256+
XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--age", "None"]))
257+
}
258+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
@testable import ArgumentParser
14+
15+
final class InputOriginTests: XCTestCase {}
16+
17+
extension InputOriginTests {
18+
func testIsDefaultValue() {
19+
func Assert(elements: [InputOrigin.Element], expectedIsDefaultValue: Bool) {
20+
let inputOrigin = InputOrigin(elements: elements)
21+
if expectedIsDefaultValue {
22+
XCTAssertTrue(inputOrigin.isDefaultValue)
23+
} else {
24+
XCTAssertFalse(inputOrigin.isDefaultValue)
25+
}
26+
}
27+
28+
Assert(elements: [], expectedIsDefaultValue: false)
29+
Assert(elements: [.defaultValue], expectedIsDefaultValue: true)
30+
Assert(elements: [.argumentIndex(SplitArguments.Index(inputIndex: 1))], expectedIsDefaultValue: false)
31+
Assert(elements: [.defaultValue, .argumentIndex(SplitArguments.Index(inputIndex: 1))], expectedIsDefaultValue: false)
32+
}
33+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
@testable import ArgumentParser
14+
15+
final class MirrorTests: XCTestCase {}
16+
17+
extension MirrorTests {
18+
private struct Foo {
19+
let foo: String?
20+
let bar: String
21+
let baz: String!
22+
}
23+
func testRealValue() {
24+
func checkChildValue(_ child: Mirror.Child, expectedString: String?) {
25+
if let expectedString = expectedString {
26+
guard let stringValue = child.value as? String else {
27+
XCTFail("child.value is not a String type")
28+
return
29+
}
30+
XCTAssertEqual(stringValue, expectedString)
31+
} else {
32+
XCTAssertNil(Mirror.realValue(for: child))
33+
// This is why we use `unwrapedOptionalValue` for optionality checks
34+
// Even though the `value` is `nil` this returns `false`
35+
XCTAssertFalse(child.value as Any? == nil)
36+
}
37+
}
38+
func performTest(foo: String?, baz: String!) {
39+
let fooChild = Foo(foo: foo, bar: "foobar", baz: baz)
40+
Mirror(reflecting: fooChild).children.forEach { child in
41+
switch child.label {
42+
case "foo":
43+
checkChildValue(child, expectedString: foo)
44+
case "bar":
45+
checkChildValue(child, expectedString: "foobar")
46+
case "baz":
47+
checkChildValue(child, expectedString: baz)
48+
default:
49+
XCTFail("Unexpected child")
50+
}
51+
}
52+
}
53+
54+
performTest(foo: "foo", baz: "baz")
55+
performTest(foo: "foo", baz: nil)
56+
performTest(foo: nil, baz: "baz")
57+
performTest(foo: nil, baz: nil)
58+
}
59+
}

0 commit comments

Comments
 (0)