Skip to content

Commit a9b9644

Browse files
authored
Stop removing underscores from CodingKey names in InputKey (#548)
When a property wrapper is applied to a property, the property's storage is given a name with a prefixed underscore. That is, for a property named `x`, the actual storage is named `_x`. That prefixed storage is what is visible through reflection, so when building an ArgumentSet from a command type's Mirror, we need to remove the leading underscore. This is done when creating an InputKey for each property. However, InputKeys are also created from CodingKeys during decoding of a ParsableCommand. These CodingKeys _do not_ have the leading underscore that is visible, so any underscores that appear are actually from the declaration of the property with an underscored name. Removing leading underscores from CodingKey names results in a mismatch when trying to find the decoded value. This change simplifies the InputKey type to use an array path instead of an indirect enum and removes the leading underscore dropping when creating an InputKey from a CodingKey. rdar://104928743
1 parent 478c2df commit a9b9644

21 files changed

+135
-165
lines changed

Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ struct BashCompletionsGenerator {
132132
///
133133
/// These consist of completions that are defined as `.list` or `.custom`.
134134
fileprivate static func generateArgumentCompletions(_ commands: [ParsableCommand.Type]) -> [String] {
135-
ArgumentSet(commands.last!, visibility: .default, parent: .root)
135+
ArgumentSet(commands.last!, visibility: .default, parent: nil)
136136
.compactMap { arg -> String? in
137137
guard arg.isPositional else { return nil }
138138

@@ -159,7 +159,7 @@ struct BashCompletionsGenerator {
159159

160160
/// Returns the case-matching statements for supplying completions after an option or flag.
161161
fileprivate static func generateOptionHandlers(_ commands: [ParsableCommand.Type]) -> String {
162-
ArgumentSet(commands.last!, visibility: .default, parent: .root)
162+
ArgumentSet(commands.last!, visibility: .default, parent: nil)
163163
.compactMap { arg -> String? in
164164
let words = arg.bashCompletionWords()
165165
if words.isEmpty { return nil }

Sources/ArgumentParser/Parsable Properties/Flag.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ extension Flag where Value: EnumerableFlag {
396396
// flag, the default value to show to the user is the `--value-name`
397397
// flag that a user would provide on the command line, not a Swift value.
398398
let defaultValueFlag = initial.flatMap { value -> String? in
399-
let defaultKey = InputKey(name: String(describing: value), parent: .key(key))
399+
let defaultKey = InputKey(name: String(describing: value), parent: key)
400400
let defaultNames = Value.name(for: value).makeNames(defaultKey)
401401
return defaultNames.first?.synopsisString
402402
}
@@ -405,7 +405,7 @@ extension Flag where Value: EnumerableFlag {
405405
let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil })
406406

407407
let args = Value.allCases.enumerated().map { (i, value) -> ArgumentDefinition in
408-
let caseKey = InputKey(name: String(describing: value), parent: .key(key))
408+
let caseKey = InputKey(name: String(describing: value), parent: key)
409409
let name = Value.name(for: value)
410410

411411
let helpForCase = caseHelps[i] ?? help
@@ -519,7 +519,7 @@ extension Flag {
519519
let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil })
520520

521521
let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in
522-
let caseKey = InputKey(name: String(describing: value), parent: .key(parentKey))
522+
let caseKey = InputKey(name: String(describing: value), parent: parentKey)
523523
let name = Element.name(for: value)
524524
let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help
525525

@@ -552,7 +552,7 @@ extension Flag {
552552
let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil })
553553

554554
let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in
555-
let caseKey = InputKey(name: String(describing: value), parent: .key(parentKey))
555+
let caseKey = InputKey(name: String(describing: value), parent: parentKey)
556556
let name = Element.name(for: value)
557557
let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help
558558
let help = ArgumentDefinition.Help(

Sources/ArgumentParser/Parsable Properties/NameSpecification.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ extension FlagInversion {
167167
case .short, .customShort:
168168
return includingShort ? element.name(for: key) : nil
169169
case .long:
170-
let modifiedKey = key.with(newName: key.name.addingIntercappedPrefix(prefix))
170+
let modifiedKey = InputKey(name: key.name.addingIntercappedPrefix(prefix), parent: key)
171171
return element.name(for: modifiedKey)
172172
case .customLong(let name, let withSingleDash):
173173
let modifiedName = name.addingPrefixWithAutodetectedStyle(prefix)

Sources/ArgumentParser/Parsable Properties/OptionGroup.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public struct OptionGroup<Value: ParsableArguments>: Decodable, ParsedWrapper {
7878
visibility: ArgumentVisibility = .default
7979
) {
8080
self.init(_parsedValue: .init { parentKey in
81-
var args = ArgumentSet(Value.self, visibility: .private, parent: .key(parentKey))
81+
var args = ArgumentSet(Value.self, visibility: .private, parent: parentKey)
8282
args.content.withEach {
8383
$0.help.parentTitle = title
8484
}

Sources/ArgumentParser/Parsable Types/ParsableArguments.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ extension ArgumentSetProvider {
248248
}
249249

250250
extension ArgumentSet {
251-
init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility, parent: InputKey.Parent) {
251+
init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility, parent: InputKey?) {
252252
#if DEBUG
253253
do {
254254
try type._validate(parent: parent)

Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
fileprivate protocol ParsableArgumentsValidator {
13-
static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError?
13+
static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError?
1414
}
1515

1616
enum ValidatorErrorKind {
@@ -37,7 +37,7 @@ struct ParsableArgumentsValidationError: Error, CustomStringConvertible {
3737
}
3838

3939
extension ParsableArguments {
40-
static func _validate(parent: InputKey.Parent) throws {
40+
static func _validate(parent: InputKey?) throws {
4141
let validators: [ParsableArgumentsValidator.Type] = [
4242
PositionalArgumentsValidator.self,
4343
ParsableArgumentsCodingKeyValidator.self,
@@ -80,7 +80,7 @@ struct PositionalArgumentsValidator: ParsableArgumentsValidator {
8080
var kind: ValidatorErrorKind { .failure }
8181
}
8282

83-
static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? {
83+
static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? {
8484
let sets: [ArgumentSet] = Mirror(reflecting: type.init())
8585
.children
8686
.compactMap { child in
@@ -190,7 +190,7 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator {
190190
}
191191
}
192192

193-
static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? {
193+
static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? {
194194
let argumentKeys: [InputKey] = Mirror(reflecting: type.init())
195195
.children
196196
.compactMap { child in
@@ -235,7 +235,7 @@ struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator {
235235
var kind: ValidatorErrorKind { .failure }
236236
}
237237

238-
static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? {
238+
static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? {
239239
let argSets: [ArgumentSet] = Mirror(reflecting: type.init())
240240
.children
241241
.compactMap { child in
@@ -283,7 +283,7 @@ struct NonsenseFlagsValidator: ParsableArgumentsValidator {
283283
var kind: ValidatorErrorKind { .warning }
284284
}
285285

286-
static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? {
286+
static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? {
287287
let argSets: [ArgumentSet] = Mirror(reflecting: type.init())
288288
.children
289289
.compactMap { child in

Sources/ArgumentParser/Parsable Types/ParsableCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ extension ParsableCommand {
166166
/// `true` if this command contains any array arguments that are declared
167167
/// with `.unconditionalRemaining`.
168168
internal static var includesUnconditionalArguments: Bool {
169-
ArgumentSet(self, visibility: .private, parent: .root).contains(where: {
169+
ArgumentSet(self, visibility: .private, parent: nil).contains(where: {
170170
$0.isRepeatingPositional && $0.parsingStrategy == .allRemainingInput
171171
})
172172
}

Sources/ArgumentParser/Parsing/ArgumentDefinition.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ extension ArgumentDefinition {
217217
///
218218
/// This initializer is used for any property defined on a `ParsableArguments`
219219
/// type that isn't decorated with one of ArgumentParser's property wrappers.
220-
init(unparsedKey: String, default defaultValue: Any?, parent: InputKey.Parent) {
220+
init(unparsedKey: String, default defaultValue: Any?, parent: InputKey?) {
221221
self.init(
222222
container: Bare<Any>.self,
223223
key: InputKey(name: unparsedKey, parent: parent),

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ extension ArgumentSet {
438438
func firstPositional(
439439
named name: String
440440
) -> ArgumentDefinition? {
441-
let key = InputKey(name: name, parent: .root)
441+
let key = InputKey(name: name, parent: nil)
442442
return first(where: { $0.help.keys.contains(key) })
443443
}
444444

Sources/ArgumentParser/Parsing/CommandParser.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ extension CommandParser {
140140
/// possible.
141141
fileprivate mutating func parseCurrent(_ split: inout SplitArguments) throws -> ParsableCommand {
142142
// Build the argument set (i.e. information on how to parse):
143-
let commandArguments = ArgumentSet(currentNode.element, visibility: .private, parent: .root)
143+
let commandArguments = ArgumentSet(currentNode.element, visibility: .private, parent: nil)
144144

145145
// Parse the arguments, ignoring anything unexpected
146146
let values = try commandArguments.lenientParse(
@@ -325,7 +325,7 @@ extension CommandParser {
325325
let completionValues = Array(args)
326326

327327
// Generate the argument set and parse the argument to find in the set
328-
let argset = ArgumentSet(current.element, visibility: .private, parent: .root)
328+
let argset = ArgumentSet(current.element, visibility: .private, parent: nil)
329329
let parsedArgument = try! parseIndividualArg(argToMatch, at: 0).first!
330330

331331
// Look up the specified argument and retrieve its custom completion function

Sources/ArgumentParser/Parsing/InputKey.swift

Lines changed: 27 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -9,122 +9,48 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
/// Represents the path to a parsed field, annotated with ``Flag``, ``Option`` or
13-
/// ``Argument``. It has a parent, which will either be ``InputKey/Parent/root``
14-
/// if the field is on the root ``ParsableComand`` or ``AsyncParsableCommand``,
15-
/// or it will have a ``InputKey/Parent/key(InputKey)`` if it is defined in
16-
/// a ``ParsableArguments`` instance.
12+
/// Represents the path to a parsed field, annotated with ``Flag``, ``Option``
13+
/// or ``Argument``. Fields that are directly declared on a ``ParsableComand``
14+
/// have a path of length 1, while fields that are declared indirectly (and
15+
/// included via an option group) have longer paths.
1716
struct InputKey: Hashable {
18-
/// Describes the parent of an ``InputKey``.
19-
indirect enum Parent: Hashable {
20-
/// There is no parent key.
21-
case root
22-
/// There is a parent key.
23-
case key(InputKey)
24-
25-
/// Initialises a parent depending on whether the key is provided.
26-
init(_ key: InputKey?) {
27-
if let key = key {
28-
self = .key(key)
29-
} else {
30-
self = .root
31-
}
32-
}
33-
}
34-
3517
/// The name of the input key.
36-
let name: String
37-
38-
/// The parent of this key.
39-
let parent: Parent
18+
var name: String
19+
20+
/// The path through the field's parents, if any.
21+
var path: [String]
4022

23+
/// The full path of the field.
24+
var fullPath: [String] { path + [name] }
4125

42-
/// Constructs a new ``InputKey``, cleaing the `name`, with the specified ``InputKey/Parent``.
26+
/// Constructs a new input key, cleaning the name, with the specified parent.
4327
///
4428
/// - Parameter name: The name of the key.
45-
/// - Parameter parent: The ``InputKey/Parent`` of the key.
46-
init(name: String, parent: Parent) {
47-
self.name = Self.clean(codingKey: name)
48-
self.parent = parent
49-
}
50-
51-
@inlinable
52-
init?(path: [CodingKey]) {
53-
var parentPath = path
54-
guard let key = parentPath.popLast() else {
55-
return nil
56-
}
57-
self.name = Self.clean(codingKey: key)
58-
self.parent = Parent(InputKey(path: parentPath))
59-
}
60-
61-
/// Constructs a new ``InputKey``, "cleaning the `value` and `path` if necessary.
62-
///
63-
/// - Parameter value: The base value of the key.
64-
/// - Parameter path: The list of ``CodingKey`` values that lead to this one. May be empty.
65-
@inlinable
66-
init(name: String, path: [CodingKey]) {
67-
self.init(name: name, parent: Parent(InputKey(path: path)))
29+
/// - Parameter parent: The input key of the parent.
30+
init(name: String, parent: InputKey?) {
31+
// Property wrappers have underscore-prefixed names, so we remove the
32+
// leading `_`, if present.
33+
self.name = name.first == "_"
34+
? String(name.dropFirst(1))
35+
: name
36+
self.path = parent?.fullPath ?? []
6837
}
6938

70-
/// Constructs a new ``InputKey``, "cleaning the `value` and `path` if necessary.
39+
/// Constructs a new input key from the given coding key and parent path.
7140
///
72-
/// - Parameter codingKey: The base ``CodingKey``
73-
/// - Parameter path: The list of ``CodingKey`` values that lead to this one. May be empty.
41+
/// - Parameter codingKey: The base ``CodingKey``. Leading underscores in
42+
/// `codingKey` is preserved.
43+
/// - Parameter path: The list of ``CodingKey`` values that lead to this one.
44+
/// `path` may be empty.
7445
@inlinable
7546
init(codingKey: CodingKey, path: [CodingKey]) {
76-
self.init(name: codingKey.stringValue, parent: Parent(InputKey(path: path)))
77-
}
78-
79-
/// The full path, including the ``parent`` and the ``name``.
80-
var fullPath: [String] {
81-
switch parent {
82-
case .root:
83-
return [name]
84-
case .key(let key):
85-
var parentPath = key.fullPath
86-
parentPath.append(name)
87-
return parentPath
88-
}
89-
}
90-
91-
/// Returns a new ``InputKey`` with the same ``path`` and a new ``name``.
92-
/// The new value will be cleaned.
93-
///
94-
/// - Parameter newName: The new ``String`` value.
95-
/// - Returns: A new ``InputKey`` with the cleaned value and the same ``path``.
96-
func with(newName: String) -> InputKey {
97-
return .init(name: Self.clean(codingKey: newName), parent: self.parent)
98-
}
99-
}
100-
101-
extension InputKey {
102-
/// Property wrappers have underscore-prefixed names, so this returns a "clean"
103-
/// version of the `codingKey`, which has the leading `'_'` removed, if present.
104-
///
105-
/// - Parameter codingKey: The key to clean.
106-
/// - Returns: The cleaned key.
107-
static func clean(codingKey: String) -> String {
108-
String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0))
109-
}
110-
111-
/// Property wrappers have underscore-prefixed names, so this returns a "clean"
112-
/// version of the `codingKey`, which has the leading `'_'` removed, if present.
113-
///
114-
/// - Parameter codingKey: The key to clean.
115-
/// - Returns: The cleaned key.
116-
static func clean(codingKey: CodingKey) -> String {
117-
clean(codingKey: codingKey.stringValue)
47+
self.name = codingKey.stringValue
48+
self.path = path.map { $0.stringValue }
11849
}
11950
}
12051

12152
extension InputKey: CustomStringConvertible {
12253
var description: String {
123-
switch parent {
124-
case .key(let parent):
125-
return "\(parent).\(name)"
126-
case .root:
127-
return name
128-
}
54+
fullPath.joined(separator: ".")
12955
}
13056
}

Sources/ArgumentParser/Usage/DumpHelpGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fileprivate extension BidirectionalCollection where Element == ParsableCommand.T
3838
/// Returns the ArgumentSet for the last command in this stack, including
3939
/// help and version flags, when appropriate.
4040
func allArguments() -> ArgumentSet {
41-
guard var arguments = self.last.map({ ArgumentSet($0, visibility: .private, parent: .root) })
41+
guard var arguments = self.last.map({ ArgumentSet($0, visibility: .private, parent: nil) })
4242
else { return ArgumentSet() }
4343
self.versionArgumentDefinition().map { arguments.append($0) }
4444
self.helpArgumentDefinition().map { arguments.append($0) }

Sources/ArgumentParser/Usage/HelpGenerator.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ internal struct HelpGenerator {
9797
fatalError()
9898
}
9999

100-
let currentArgSet = ArgumentSet(currentCommand, visibility: visibility, parent: .root)
100+
let currentArgSet = ArgumentSet(currentCommand, visibility: visibility, parent: nil)
101101
self.commandStack = commandStack
102102

103103
// Build the tool name and subcommand name from the command configuration
@@ -292,7 +292,7 @@ fileprivate extension NameSpecification {
292292
/// step, the name are returned in descending order.
293293
func generateHelpNames(visibility: ArgumentVisibility) -> [Name] {
294294
self
295-
.makeNames(InputKey(name: "help", parent: .root))
295+
.makeNames(InputKey(name: "help", parent: nil))
296296
.compactMap { name in
297297
guard visibility.base != .default else { return name }
298298
switch name {
@@ -333,7 +333,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type
333333
options: [.isOptional],
334334
help: "Show the version.",
335335
defaultValue: nil,
336-
key: InputKey(name: "", parent: .root),
336+
key: InputKey(name: "", parent: nil),
337337
isComposite: false),
338338
completion: .default,
339339
update: .nullary({ _, _, _ in })
@@ -350,7 +350,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type
350350
options: [.isOptional],
351351
help: "Show help information.",
352352
defaultValue: nil,
353-
key: InputKey(name: "", parent: .root),
353+
key: InputKey(name: "", parent: nil),
354354
isComposite: false),
355355
completion: .default,
356356
update: .nullary({ _, _, _ in })
@@ -365,7 +365,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type
365365
options: [.isOptional],
366366
help: ArgumentHelp("Dump help information as JSON."),
367367
defaultValue: nil,
368-
key: InputKey(name: "", parent: .root),
368+
key: InputKey(name: "", parent: nil),
369369
isComposite: false),
370370
completion: .default,
371371
update: .nullary({ _, _, _ in })
@@ -375,7 +375,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type
375375
/// Returns the ArgumentSet for the last command in this stack, including
376376
/// help and version flags, when appropriate.
377377
func argumentsForHelp(visibility: ArgumentVisibility) -> ArgumentSet {
378-
guard var arguments = self.last.map({ ArgumentSet($0, visibility: visibility, parent: .root) })
378+
guard var arguments = self.last.map({ ArgumentSet($0, visibility: visibility, parent: nil) })
379379
else { return ArgumentSet() }
380380
self.versionArgumentDefinition().map { arguments.append($0) }
381381
self.helpArgumentDefinition().map { arguments.append($0) }

0 commit comments

Comments
 (0)