From a077ca597804d167c860f2cae891d105e90049a6 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 2 Feb 2023 01:34:01 -0600 Subject: [PATCH] Stop removing underscores from CodingKey names in InputKey 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 --- .../BashCompletionsGenerator.swift | 4 +- .../Parsable Properties/Flag.swift | 8 +- .../NameSpecification.swift | 2 +- .../Parsable Properties/OptionGroup.swift | 2 +- .../Parsable Types/ParsableArguments.swift | 2 +- .../ParsableArgumentsValidation.swift | 12 +- .../Parsable Types/ParsableCommand.swift | 2 +- .../Parsing/ArgumentDefinition.swift | 2 +- .../ArgumentParser/Parsing/ArgumentSet.swift | 2 +- .../Parsing/CommandParser.swift | 4 +- Sources/ArgumentParser/Parsing/InputKey.swift | 128 ++++-------------- .../Usage/DumpHelpGenerator.swift | 2 +- .../ArgumentParser/Usage/HelpGenerator.swift | 12 +- .../ArgumentParser/Usage/MessageInfo.swift | 2 +- .../ArgumentParser/Usage/UsageGenerator.swift | 2 +- .../DefaultsEndToEndTests.swift | 30 ++++ .../OptionGroupEndToEndTests.swift | 50 ++++--- .../HelpGenerationTests.swift | 2 +- .../NameSpecificationTests.swift | 6 +- .../ParsableArgumentsValidationTests.swift | 24 ++-- .../UsageGenerationTests.swift | 2 +- 21 files changed, 135 insertions(+), 165 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 753ebcf51..1fea1b73e 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -132,7 +132,7 @@ struct BashCompletionsGenerator { /// /// These consist of completions that are defined as `.list` or `.custom`. fileprivate static func generateArgumentCompletions(_ commands: [ParsableCommand.Type]) -> [String] { - ArgumentSet(commands.last!, visibility: .default, parent: .root) + ArgumentSet(commands.last!, visibility: .default, parent: nil) .compactMap { arg -> String? in guard arg.isPositional else { return nil } @@ -159,7 +159,7 @@ struct BashCompletionsGenerator { /// Returns the case-matching statements for supplying completions after an option or flag. fileprivate static func generateOptionHandlers(_ commands: [ParsableCommand.Type]) -> String { - ArgumentSet(commands.last!, visibility: .default, parent: .root) + ArgumentSet(commands.last!, visibility: .default, parent: nil) .compactMap { arg -> String? in let words = arg.bashCompletionWords() if words.isEmpty { return nil } diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index 334df8874..74a97f795 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -396,7 +396,7 @@ extension Flag where Value: EnumerableFlag { // flag, the default value to show to the user is the `--value-name` // flag that a user would provide on the command line, not a Swift value. let defaultValueFlag = initial.flatMap { value -> String? in - let defaultKey = InputKey(name: String(describing: value), parent: .key(key)) + let defaultKey = InputKey(name: String(describing: value), parent: key) let defaultNames = Value.name(for: value).makeNames(defaultKey) return defaultNames.first?.synopsisString } @@ -405,7 +405,7 @@ extension Flag where Value: EnumerableFlag { let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil }) let args = Value.allCases.enumerated().map { (i, value) -> ArgumentDefinition in - let caseKey = InputKey(name: String(describing: value), parent: .key(key)) + let caseKey = InputKey(name: String(describing: value), parent: key) let name = Value.name(for: value) let helpForCase = caseHelps[i] ?? help @@ -519,7 +519,7 @@ extension Flag { let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil }) let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in - let caseKey = InputKey(name: String(describing: value), parent: .key(parentKey)) + let caseKey = InputKey(name: String(describing: value), parent: parentKey) let name = Element.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help @@ -552,7 +552,7 @@ extension Flag { let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil }) let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in - let caseKey = InputKey(name: String(describing: value), parent: .key(parentKey)) + let caseKey = InputKey(name: String(describing: value), parent: parentKey) let name = Element.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help let help = ArgumentDefinition.Help( diff --git a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift index bfdd48c4a..a71d9df6a 100644 --- a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift +++ b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift @@ -167,7 +167,7 @@ extension FlagInversion { case .short, .customShort: return includingShort ? element.name(for: key) : nil case .long: - let modifiedKey = key.with(newName: key.name.addingIntercappedPrefix(prefix)) + let modifiedKey = InputKey(name: key.name.addingIntercappedPrefix(prefix), parent: key) return element.name(for: modifiedKey) case .customLong(let name, let withSingleDash): let modifiedName = name.addingPrefixWithAutodetectedStyle(prefix) diff --git a/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift index b74865b62..13192f8ed 100644 --- a/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift +++ b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift @@ -78,7 +78,7 @@ public struct OptionGroup: Decodable, ParsedWrapper { visibility: ArgumentVisibility = .default ) { self.init(_parsedValue: .init { parentKey in - var args = ArgumentSet(Value.self, visibility: .private, parent: .key(parentKey)) + var args = ArgumentSet(Value.self, visibility: .private, parent: parentKey) args.content.withEach { $0.help.parentTitle = title } diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index b2e572a6e..d85283c37 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -248,7 +248,7 @@ extension ArgumentSetProvider { } extension ArgumentSet { - init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility, parent: InputKey.Parent) { + init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility, parent: InputKey?) { #if DEBUG do { try type._validate(parent: parent) diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift index f633d91f7..c03e9d3af 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift @@ -10,7 +10,7 @@ //===----------------------------------------------------------------------===// fileprivate protocol ParsableArgumentsValidator { - static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? + static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? } enum ValidatorErrorKind { @@ -37,7 +37,7 @@ struct ParsableArgumentsValidationError: Error, CustomStringConvertible { } extension ParsableArguments { - static func _validate(parent: InputKey.Parent) throws { + static func _validate(parent: InputKey?) throws { let validators: [ParsableArgumentsValidator.Type] = [ PositionalArgumentsValidator.self, ParsableArgumentsCodingKeyValidator.self, @@ -80,7 +80,7 @@ struct PositionalArgumentsValidator: ParsableArgumentsValidator { var kind: ValidatorErrorKind { .failure } } - static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { + static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? { let sets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in @@ -190,7 +190,7 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { } } - static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { + static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? { let argumentKeys: [InputKey] = Mirror(reflecting: type.init()) .children .compactMap { child in @@ -235,7 +235,7 @@ struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator { var kind: ValidatorErrorKind { .failure } } - static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { + static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? { let argSets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in @@ -283,7 +283,7 @@ struct NonsenseFlagsValidator: ParsableArgumentsValidator { var kind: ValidatorErrorKind { .warning } } - static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { + static func validate(_ type: ParsableArguments.Type, parent: InputKey?) -> ParsableArgumentsValidatorError? { let argSets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in diff --git a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift index 288ea7f6e..af835a007 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @@ -166,7 +166,7 @@ extension ParsableCommand { /// `true` if this command contains any array arguments that are declared /// with `.unconditionalRemaining`. internal static var includesUnconditionalArguments: Bool { - ArgumentSet(self, visibility: .private, parent: .root).contains(where: { + ArgumentSet(self, visibility: .private, parent: nil).contains(where: { $0.isRepeatingPositional && $0.parsingStrategy == .allRemainingInput }) } diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index bdbda9a26..d1036b39b 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -217,7 +217,7 @@ extension ArgumentDefinition { /// /// This initializer is used for any property defined on a `ParsableArguments` /// type that isn't decorated with one of ArgumentParser's property wrappers. - init(unparsedKey: String, default defaultValue: Any?, parent: InputKey.Parent) { + init(unparsedKey: String, default defaultValue: Any?, parent: InputKey?) { self.init( container: Bare.self, key: InputKey(name: unparsedKey, parent: parent), diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 6adfebf99..dc689dc5e 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -438,7 +438,7 @@ extension ArgumentSet { func firstPositional( named name: String ) -> ArgumentDefinition? { - let key = InputKey(name: name, parent: .root) + let key = InputKey(name: name, parent: nil) return first(where: { $0.help.keys.contains(key) }) } diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index ee327c41f..5c17cf228 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -140,7 +140,7 @@ extension CommandParser { /// possible. fileprivate mutating func parseCurrent(_ split: inout SplitArguments) throws -> ParsableCommand { // Build the argument set (i.e. information on how to parse): - let commandArguments = ArgumentSet(currentNode.element, visibility: .private, parent: .root) + let commandArguments = ArgumentSet(currentNode.element, visibility: .private, parent: nil) // Parse the arguments, ignoring anything unexpected let values = try commandArguments.lenientParse( @@ -325,7 +325,7 @@ extension CommandParser { let completionValues = Array(args) // Generate the argument set and parse the argument to find in the set - let argset = ArgumentSet(current.element, visibility: .private, parent: .root) + let argset = ArgumentSet(current.element, visibility: .private, parent: nil) let parsedArgument = try! parseIndividualArg(argToMatch, at: 0).first! // Look up the specified argument and retrieve its custom completion function diff --git a/Sources/ArgumentParser/Parsing/InputKey.swift b/Sources/ArgumentParser/Parsing/InputKey.swift index 1d47d17cf..9837e41e0 100644 --- a/Sources/ArgumentParser/Parsing/InputKey.swift +++ b/Sources/ArgumentParser/Parsing/InputKey.swift @@ -9,122 +9,48 @@ // //===----------------------------------------------------------------------===// -/// Represents the path to a parsed field, annotated with ``Flag``, ``Option`` or -/// ``Argument``. It has a parent, which will either be ``InputKey/Parent/root`` -/// if the field is on the root ``ParsableComand`` or ``AsyncParsableCommand``, -/// or it will have a ``InputKey/Parent/key(InputKey)`` if it is defined in -/// a ``ParsableArguments`` instance. +/// Represents the path to a parsed field, annotated with ``Flag``, ``Option`` +/// or ``Argument``. Fields that are directly declared on a ``ParsableComand`` +/// have a path of length 1, while fields that are declared indirectly (and +/// included via an option group) have longer paths. struct InputKey: Hashable { - /// Describes the parent of an ``InputKey``. - indirect enum Parent: Hashable { - /// There is no parent key. - case root - /// There is a parent key. - case key(InputKey) - - /// Initialises a parent depending on whether the key is provided. - init(_ key: InputKey?) { - if let key = key { - self = .key(key) - } else { - self = .root - } - } - } - /// The name of the input key. - let name: String - - /// The parent of this key. - let parent: Parent + var name: String + + /// The path through the field's parents, if any. + var path: [String] + /// The full path of the field. + var fullPath: [String] { path + [name] } - /// Constructs a new ``InputKey``, cleaing the `name`, with the specified ``InputKey/Parent``. + /// Constructs a new input key, cleaning the name, with the specified parent. /// /// - Parameter name: The name of the key. - /// - Parameter parent: The ``InputKey/Parent`` of the key. - init(name: String, parent: Parent) { - self.name = Self.clean(codingKey: name) - self.parent = parent - } - - @inlinable - init?(path: [CodingKey]) { - var parentPath = path - guard let key = parentPath.popLast() else { - return nil - } - self.name = Self.clean(codingKey: key) - self.parent = Parent(InputKey(path: parentPath)) - } - - /// Constructs a new ``InputKey``, "cleaning the `value` and `path` if necessary. - /// - /// - Parameter value: The base value of the key. - /// - Parameter path: The list of ``CodingKey`` values that lead to this one. May be empty. - @inlinable - init(name: String, path: [CodingKey]) { - self.init(name: name, parent: Parent(InputKey(path: path))) + /// - Parameter parent: The input key of the parent. + init(name: String, parent: InputKey?) { + // Property wrappers have underscore-prefixed names, so we remove the + // leading `_`, if present. + self.name = name.first == "_" + ? String(name.dropFirst(1)) + : name + self.path = parent?.fullPath ?? [] } - /// Constructs a new ``InputKey``, "cleaning the `value` and `path` if necessary. + /// Constructs a new input key from the given coding key and parent path. /// - /// - Parameter codingKey: The base ``CodingKey`` - /// - Parameter path: The list of ``CodingKey`` values that lead to this one. May be empty. + /// - Parameter codingKey: The base ``CodingKey``. Leading underscores in + /// `codingKey` is preserved. + /// - Parameter path: The list of ``CodingKey`` values that lead to this one. + /// `path` may be empty. @inlinable init(codingKey: CodingKey, path: [CodingKey]) { - self.init(name: codingKey.stringValue, parent: Parent(InputKey(path: path))) - } - - /// The full path, including the ``parent`` and the ``name``. - var fullPath: [String] { - switch parent { - case .root: - return [name] - case .key(let key): - var parentPath = key.fullPath - parentPath.append(name) - return parentPath - } - } - - /// Returns a new ``InputKey`` with the same ``path`` and a new ``name``. - /// The new value will be cleaned. - /// - /// - Parameter newName: The new ``String`` value. - /// - Returns: A new ``InputKey`` with the cleaned value and the same ``path``. - func with(newName: String) -> InputKey { - return .init(name: Self.clean(codingKey: newName), parent: self.parent) - } -} - -extension InputKey { - /// Property wrappers have underscore-prefixed names, so this returns a "clean" - /// version of the `codingKey`, which has the leading `'_'` removed, if present. - /// - /// - Parameter codingKey: The key to clean. - /// - Returns: The cleaned key. - static func clean(codingKey: String) -> String { - String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) - } - - /// Property wrappers have underscore-prefixed names, so this returns a "clean" - /// version of the `codingKey`, which has the leading `'_'` removed, if present. - /// - /// - Parameter codingKey: The key to clean. - /// - Returns: The cleaned key. - static func clean(codingKey: CodingKey) -> String { - clean(codingKey: codingKey.stringValue) + self.name = codingKey.stringValue + self.path = path.map { $0.stringValue } } } extension InputKey: CustomStringConvertible { var description: String { - switch parent { - case .key(let parent): - return "\(parent).\(name)" - case .root: - return name - } + fullPath.joined(separator: ".") } } diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 658fc9efe..efd0442ab 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -38,7 +38,7 @@ fileprivate extension BidirectionalCollection where Element == ParsableCommand.T /// Returns the ArgumentSet for the last command in this stack, including /// help and version flags, when appropriate. func allArguments() -> ArgumentSet { - guard var arguments = self.last.map({ ArgumentSet($0, visibility: .private, parent: .root) }) + guard var arguments = self.last.map({ ArgumentSet($0, visibility: .private, parent: nil) }) else { return ArgumentSet() } self.versionArgumentDefinition().map { arguments.append($0) } self.helpArgumentDefinition().map { arguments.append($0) } diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 7bc6020c6..440f73f3e 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -97,7 +97,7 @@ internal struct HelpGenerator { fatalError() } - let currentArgSet = ArgumentSet(currentCommand, visibility: visibility, parent: .root) + let currentArgSet = ArgumentSet(currentCommand, visibility: visibility, parent: nil) self.commandStack = commandStack // Build the tool name and subcommand name from the command configuration @@ -292,7 +292,7 @@ fileprivate extension NameSpecification { /// step, the name are returned in descending order. func generateHelpNames(visibility: ArgumentVisibility) -> [Name] { self - .makeNames(InputKey(name: "help", parent: .root)) + .makeNames(InputKey(name: "help", parent: nil)) .compactMap { name in guard visibility.base != .default else { return name } switch name { @@ -333,7 +333,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type options: [.isOptional], help: "Show the version.", defaultValue: nil, - key: InputKey(name: "", parent: .root), + key: InputKey(name: "", parent: nil), isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) @@ -350,7 +350,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type options: [.isOptional], help: "Show help information.", defaultValue: nil, - key: InputKey(name: "", parent: .root), + key: InputKey(name: "", parent: nil), isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) @@ -365,7 +365,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type options: [.isOptional], help: ArgumentHelp("Dump help information as JSON."), defaultValue: nil, - key: InputKey(name: "", parent: .root), + key: InputKey(name: "", parent: nil), isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) @@ -375,7 +375,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type /// Returns the ArgumentSet for the last command in this stack, including /// help and version flags, when appropriate. func argumentsForHelp(visibility: ArgumentVisibility) -> ArgumentSet { - guard var arguments = self.last.map({ ArgumentSet($0, visibility: visibility, parent: .root) }) + guard var arguments = self.last.map({ ArgumentSet($0, visibility: visibility, parent: nil) }) else { return ArgumentSet() } self.versionArgumentDefinition().map { arguments.append($0) } self.helpArgumentDefinition().map { arguments.append($0) } diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index 68d4a01cd..896c565b7 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -122,7 +122,7 @@ enum MessageInfo { guard case ParserError.noArguments = parserError else { return usage } return "\n" + HelpGenerator(commandStack: [type.asCommand], visibility: .default).rendered() }() - let argumentSet = ArgumentSet(commandStack.last!, visibility: .default, parent: .root) + let argumentSet = ArgumentSet(commandStack.last!, visibility: .default, parent: nil) let message = argumentSet.errorDescription(error: parserError) ?? "" let helpAbstract = argumentSet.helpDescription(error: parserError) ?? "" self = .validation(message: message, usage: usage, help: helpAbstract) diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index b80bdee15..baf46c044 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -22,7 +22,7 @@ extension UsageGenerator { self.init(toolName: toolName, definition: definition) } - init(toolName: String, parsable: ParsableArguments, visibility: ArgumentVisibility, parent: InputKey.Parent) { + init(toolName: String, parsable: ParsableArguments, visibility: ArgumentVisibility, parent: InputKey?) { self.init( toolName: toolName, definition: ArgumentSet(type(of: parsable), visibility: visibility, parent: parent)) diff --git a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift index 4c396c581..4a8e7b7df 100644 --- a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift @@ -828,3 +828,33 @@ extension DefaultsEndToEndTests { } } } + +extension DefaultsEndToEndTests { + private struct UnderscoredOptional: ParsableCommand { + @Option(name: .customLong("arg")) + var _arg: String? + } + + private struct UnderscoredArray: ParsableCommand { + @Option(name: .customLong("columns"), parsing: .upToNextOption) + var _columns: [String] = [] + } + + func testUnderscoredOptional() throws { + AssertParse(UnderscoredOptional.self, []) { parsed in + XCTAssertNil(parsed._arg) + } + AssertParse(UnderscoredOptional.self, ["--arg", "foo"]) { parsed in + XCTAssertEqual(parsed._arg, "foo") + } + } + + func testUnderscoredArray() throws { + AssertParse(UnderscoredArray.self, []) { parsed in + XCTAssertEqual(parsed._columns, []) + } + AssertParse(UnderscoredArray.self, ["--columns", "foo", "bar", "baz"]) { parsed in + XCTAssertEqual(parsed._columns, ["foo", "bar", "baz"]) + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift index c47ac2776..0968157c8 100644 --- a/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift @@ -109,52 +109,66 @@ extension OptionGroupEndToEndTests { } } -fileprivate struct DuplicatedFlagOption: ParsableArguments { +fileprivate struct DuplicatedFlagGroupCustom: ParsableArguments { @Flag(name: .customLong("duplicated-option")) var duplicated: Bool = false - - enum CodingKeys: CodingKey { - case duplicated - } } -fileprivate struct DuplicatedFlagCommand: ParsableCommand { +fileprivate struct DuplicatedFlagGroupCustomCommand: ParsableCommand { + @Flag var duplicated: Bool = false + @OptionGroup var option: DuplicatedFlagGroupCustom +} + +fileprivate struct DuplicatedFlagGroupLong: ParsableArguments { + @Flag var duplicated: Bool = false +} - @Flag +fileprivate struct DuplicatedFlagGroupLongCommand: ParsableCommand { + @Flag(name: .customLong("duplicated-option")) var duplicated: Bool = false - - @OptionGroup var option: DuplicatedFlagOption - - enum CodingKeys: CodingKey { - case duplicated - case option - } + @OptionGroup var option: DuplicatedFlagGroupLong } extension OptionGroupEndToEndTests { func testUniqueNamesForDuplicatedFlag_NoFlags() throws { - AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.self, []) { command in + AssertParse(DuplicatedFlagGroupCustomCommand.self, []) { command in + XCTAssertFalse(command.duplicated) + XCTAssertFalse(command.option.duplicated) + } + AssertParse(DuplicatedFlagGroupLongCommand.self, []) { command in XCTAssertFalse(command.duplicated) XCTAssertFalse(command.option.duplicated) } } func testUniqueNamesForDuplicatedFlag_RootOnly() throws { - AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.self, ["--duplicated"]) { command in + AssertParse(DuplicatedFlagGroupCustomCommand.self, ["--duplicated"]) { command in XCTAssertTrue(command.duplicated) XCTAssertFalse(command.option.duplicated) } + AssertParse(DuplicatedFlagGroupLongCommand.self, ["--duplicated"]) { command in + XCTAssertFalse(command.duplicated) + XCTAssertTrue(command.option.duplicated) + } } func testUniqueNamesForDuplicatedFlag_OptionOnly() throws { - AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.self, ["--duplicated-option"]) { command in + AssertParse(DuplicatedFlagGroupCustomCommand.self, ["--duplicated-option"]) { command in XCTAssertFalse(command.duplicated) XCTAssertTrue(command.option.duplicated) } + AssertParse(DuplicatedFlagGroupLongCommand.self, ["--duplicated-option"]) { command in + XCTAssertTrue(command.duplicated) + XCTAssertFalse(command.option.duplicated) + } } func testUniqueNamesForDuplicatedFlag_RootAndOption() throws { - AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.self, ["--duplicated", "--duplicated-option"]) { command in + AssertParse(DuplicatedFlagGroupCustomCommand.self, ["--duplicated", "--duplicated-option"]) { command in + XCTAssertTrue(command.duplicated) + XCTAssertTrue(command.option.duplicated) + } + AssertParse(DuplicatedFlagGroupLongCommand.self, ["--duplicated", "--duplicated-option"]) { command in XCTAssertTrue(command.duplicated) XCTAssertTrue(command.option.duplicated) } diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index 1b3d7435c..476eb67a8 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -638,7 +638,7 @@ extension HelpGenerationTests { } func testAllValues() { - let opts = ArgumentSet(AllValues.self, visibility: .private, parent: .root) + let opts = ArgumentSet(AllValues.self, visibility: .private, parent: nil) XCTAssertEqual(AllValues.Manual.allValueStrings, opts[0].help.allValues) XCTAssertEqual(AllValues.Manual.allValueStrings, opts[1].help.allValues) diff --git a/Tests/ArgumentParserUnitTests/NameSpecificationTests.swift b/Tests/ArgumentParserUnitTests/NameSpecificationTests.swift index b0b6c905d..328b77afb 100644 --- a/Tests/ArgumentParserUnitTests/NameSpecificationTests.swift +++ b/Tests/ArgumentParserUnitTests/NameSpecificationTests.swift @@ -17,7 +17,7 @@ final class NameSpecificationTests: XCTestCase { extension NameSpecificationTests { func testFlagNames_withNoPrefix() { - let key = InputKey(name: "index", parent: .root) + let key = InputKey(name: "index", parent: nil) XCTAssertEqual(FlagInversion.prefixedNo.enableDisableNamePair(for: key, name: .customLong("foo")).1, [.long("no-foo")]) XCTAssertEqual(FlagInversion.prefixedNo.enableDisableNamePair(for: key, name: .customLong("foo-bar-baz")).1, [.long("no-foo-bar-baz")]) @@ -26,7 +26,7 @@ extension NameSpecificationTests { } func testFlagNames_withEnableDisablePrefix() { - let key = InputKey(name: "index", parent: .root) + let key = InputKey(name: "index", parent: nil) XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .long).0, [.long("enable-index")]) XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .long).1, [.long("disable-index")]) @@ -42,7 +42,7 @@ extension NameSpecificationTests { } } -fileprivate func Assert(nameSpecification: NameSpecification, key: String, parent: InputKey.Parent = .root, makeNames expected: [Name], file: StaticString = #file, line: UInt = #line) { +fileprivate func Assert(nameSpecification: NameSpecification, key: String, parent: InputKey? = nil, makeNames expected: [Name], file: StaticString = #file, line: UInt = #line) { let names = nameSpecification.makeNames(InputKey(name: key, parent: parent)) Assert(names: names, expected: expected, file: file, line: line) } diff --git a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift index cb2ba5f6e..16a6ecfb4 100644 --- a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift +++ b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift @@ -81,7 +81,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testCodingKeyValidation() throws { - let parent = InputKey.Parent.key(InputKey(name: "parentKey", parent: .root)) + let parent = InputKey(name: "parentKey", parent: nil) XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(A.self, parent: parent)) XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(B.self, parent: parent)) @@ -130,7 +130,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testCustomDecoderValidation() throws { - let parent = InputKey.Parent(InputKey(name: "foo", parent: .root)) + let parent = InputKey(name: "foo", parent: nil) if let error = ParsableArgumentsCodingKeyValidator.validate(TypeWithInvalidDecoder.self, parent: parent) as? ParsableArgumentsCodingKeyValidator.InvalidDecoderError { @@ -211,7 +211,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testPositionalArgumentsValidation() throws { - let parent = InputKey.Parent(InputKey(name: "foo", parent: .root)) + let parent = InputKey(name: "foo", parent: nil) XCTAssertNil(PositionalArgumentsValidator.validate(A.self, parent: parent)) XCTAssertNil(PositionalArgumentsValidator.validate(F.self, parent: parent)) XCTAssertNil(PositionalArgumentsValidator.validate(H.self, parent: parent)) @@ -246,7 +246,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_NoViolation() throws { - let parent = InputKey.Parent(InputKey(name: "foo", parent: .root)) + let parent = InputKey(name: "foo", parent: nil) XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DifferentNames.self, parent: parent)) } @@ -260,7 +260,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_TwoOfSameName() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(TwoOfTheSameName.self, parent: .root) + if let error = ParsableArgumentsUniqueNamesValidator.validate(TwoOfTheSameName.self, parent: nil) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"--foo\".") @@ -288,7 +288,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_TwoDuplications() throws { - let parent = InputKey.Parent(InputKey(name: "option", parent: .root)) + let parent = InputKey(name: "option", parent: nil) if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleUniquenessViolations.self, parent: parent) as? ParsableArgumentsUniqueNamesValidator.Error { @@ -324,7 +324,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_ArgumentHasMultipleNames() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleNamesPerArgument.self, parent: .root) + if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleNamesPerArgument.self, parent: nil) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"-v\".") @@ -355,7 +355,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_MoreThanTwoDuplications() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(FourDuplicateNames.self, parent: .root) + if let error = ParsableArgumentsUniqueNamesValidator.validate(FourDuplicateNames.self, parent: nil) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (4) `Option` or `Flag` arguments are named \"--foo\".") @@ -397,7 +397,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_DuplicatedFlagFirstLetters_ShortNames() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersShortNames.self, parent: .root) + if let error = ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersShortNames.self, parent: nil) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (3) `Option` or `Flag` arguments are named \"-f\".") @@ -407,7 +407,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_DuplicatedFlagFirstLetters_LongNames() throws { - XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self, parent: .root)) + XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self, parent: nil)) } fileprivate struct HasOneNonsenseFlag: ParsableCommand { @@ -439,7 +439,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testNonsenseFlagsValidation_OneFlag() throws { - if let error = NonsenseFlagsValidator.validate(HasOneNonsenseFlag.self, parent: .root) + if let error = NonsenseFlagsValidator.validate(HasOneNonsenseFlag.self, parent: nil) as? NonsenseFlagsValidator.Error { XCTAssertEqual( @@ -476,7 +476,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testNonsenseFlagsValidation_MultipleFlags() throws { - if let error = NonsenseFlagsValidator.validate(MultipleNonsenseFlags.self, parent: .root) + if let error = NonsenseFlagsValidator.validate(MultipleNonsenseFlags.self, parent: nil) as? NonsenseFlagsValidator.Error { XCTAssertEqual( diff --git a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift index 7f57f6102..2c605aab8 100644 --- a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift @@ -22,7 +22,7 @@ func _testSynopsis( file: StaticString = #file, line: UInt = #line ) { - let help = UsageGenerator(toolName: "example", parsable: T(), visibility: visibility, parent: .root) + let help = UsageGenerator(toolName: "example", parsable: T(), visibility: visibility, parent: nil) XCTAssertEqual(help.synopsis, expected, file: file, line: line) }