From 587d26a2aa5d962b8845805378fa93b7c270d820 Mon Sep 17 00:00:00 2001 From: David Peterson Date: Fri, 23 Sep 2022 00:43:48 +1000 Subject: [PATCH] Fixes incorrect value copying when ParseArguments has the same field name+type as the ParseCommand (Issue #322) (#495) * Contains fixes and test cases for #322 --- .../BashCompletionsGenerator.swift | 6 +- .../Completions/CompletionsGenerator.swift | 2 +- .../Parsable Properties/Flag.swift | 24 ++-- .../NameSpecification.swift | 6 +- .../Parsable Properties/OptionGroup.swift | 4 +- .../Parsable Types/ParsableArguments.swift | 14 +- .../ParsableArgumentsValidation.swift | 52 +++---- .../Parsable Types/ParsableCommand.swift | 2 +- .../Parsing/ArgumentDecoder.swift | 19 ++- .../Parsing/ArgumentDefinition.swift | 6 +- .../ArgumentParser/Parsing/ArgumentSet.swift | 2 +- .../Parsing/CommandParser.swift | 4 +- Sources/ArgumentParser/Parsing/InputKey.swift | 130 ++++++++++++++++++ .../ArgumentParser/Parsing/ParsedValues.swift | 14 -- .../Usage/DumpHelpGenerator.swift | 2 +- .../ArgumentParser/Usage/HelpGenerator.swift | 12 +- .../ArgumentParser/Usage/MessageInfo.swift | 2 +- .../ArgumentParser/Usage/UsageGenerator.swift | 4 +- .../OptionGroupEndToEndTests.swift | 53 +++++++ .../HelpGenerationTests.swift | 2 +- .../NameSpecificationTests.swift | 8 +- .../ParsableArgumentsValidationTests.swift | 68 +++++---- .../UsageGenerationTests.swift | 2 +- 23 files changed, 301 insertions(+), 137 deletions(-) create mode 100644 Sources/ArgumentParser/Parsing/InputKey.swift diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index e444f81dd..753ebcf51 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) + ArgumentSet(commands.last!, visibility: .default, parent: .root) .compactMap { arg -> String? in guard arg.isPositional else { return nil } @@ -148,7 +148,7 @@ struct BashCompletionsGenerator { let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ") // TODO: Make this work for @Arguments let argumentName = arg.names.preferredName?.synopsisString - ?? arg.help.keys.first?.rawValue ?? "---" + ?? arg.help.keys.first?.name ?? "---" return """ $("${COMP_WORDS[0]}" ---completion \(subcommandNames) -- \(argumentName) "${COMP_WORDS[@]}") @@ -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) + ArgumentSet(commands.last!, visibility: .default, parent: .root) .compactMap { arg -> String? in let words = arg.bashCompletionWords() if words.isEmpty { return nil } diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 11f3a0b7a..5150f69a8 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -105,7 +105,7 @@ extension ArgumentDefinition { func customCompletionCall(_ commands: [ParsableCommand.Type]) -> String { let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ") let argumentName = names.preferredName?.synopsisString - ?? self.help.keys.first?.rawValue ?? "---" + ?? self.help.keys.first?.name ?? "---" return "---completion \(subcommandNames) -- \(argumentName)" } } diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index f9dce7113..334df8874 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(rawValue: String(describing: value)) + let defaultKey = InputKey(name: String(describing: value), parent: .key(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(rawValue: String(describing: value)) + let caseKey = InputKey(name: String(describing: value), parent: .key(key)) let name = Value.name(for: value) let helpForCase = caseHelps[i] ?? help @@ -510,7 +510,7 @@ extension Flag { exclusivity: FlagExclusivity = .exclusive, help: ArgumentHelp? = nil ) where Value == Element?, Element: EnumerableFlag { - self.init(_parsedValue: .init { key in + self.init(_parsedValue: .init { parentKey in // This gets flipped to `true` the first time one of these flags is // encountered. var hasUpdated = false @@ -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(rawValue: String(describing: value)) + let caseKey = InputKey(name: String(describing: value), parent: .key(parentKey)) let name = Element.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help @@ -528,11 +528,11 @@ extension Flag { options: [.isOptional], help: helpForCase, defaultValue: nil, - key: key, + key: parentKey, isComposite: !hasCustomCaseHelp) - return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in - hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) + return ArgumentDefinition.flag(name: name, key: parentKey, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in + hasUpdated = try ArgumentSet.updateFlag(key: parentKey, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) })) } @@ -547,12 +547,12 @@ extension Flag { initial: [Element]?, help: ArgumentHelp? = nil ) where Value == Array, Element: EnumerableFlag { - self.init(_parsedValue: .init { key in + self.init(_parsedValue: .init { parentKey in let caseHelps = Element.allCases.map { Element.help(for: $0) } let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil }) let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in - let caseKey = InputKey(rawValue: String(describing: value)) + let caseKey = InputKey(name: String(describing: value), parent: .key(parentKey)) let name = Element.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help let help = ArgumentDefinition.Help( @@ -560,11 +560,11 @@ extension Flag { options: [.isOptional], help: helpForCase, defaultValue: nil, - key: key, + key: parentKey, isComposite: !hasCustomCaseHelp) - return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: initial, update: .nullary({ (origin, name, values) in - values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + return ArgumentDefinition.flag(name: name, key: parentKey, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: initial, update: .nullary({ (origin, name, values) in + values.update(forKey: parentKey, inputOrigin: origin, initial: [Element](), closure: { $0.append(value) }) })) diff --git a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift index 7a71c24a7..bfdd48c4a 100644 --- a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift +++ b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift @@ -136,9 +136,9 @@ extension NameSpecification.Element { internal func name(for key: InputKey) -> Name? { switch self.base { case .long: - return .long(key.rawValue.convertedToSnakeCase(separator: "-")) + return .long(key.name.convertedToSnakeCase(separator: "-")) case .short: - guard let c = key.rawValue.first else { fatalError("Key '\(key.rawValue)' has not characters to form short option name.") } + guard let c = key.name.first else { fatalError("Key '\(key.name)' has not characters to form short option name.") } return .short(c) case .customLong(let name, let withSingleDash): return withSingleDash @@ -167,7 +167,7 @@ extension FlagInversion { case .short, .customShort: return includingShort ? element.name(for: key) : nil case .long: - let modifiedKey = InputKey(rawValue: key.rawValue.addingIntercappedPrefix(prefix)) + let modifiedKey = key.with(newName: key.name.addingIntercappedPrefix(prefix)) 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 bb8555614..f727fec75 100644 --- a/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift +++ b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift @@ -64,8 +64,8 @@ public struct OptionGroup: Decodable, ParsedWrapper { /// Creates a property that represents another parsable type, using the /// specified visibility. public init(visibility: ArgumentVisibility = .default) { - self.init(_parsedValue: .init { _ in - ArgumentSet(Value.self, visibility: .private) + self.init(_parsedValue: .init { parentKey in + return ArgumentSet(Value.self, visibility: .private, parent: .key(parentKey)) }) self._visibility = visibility } diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index 88b51e20c..22657e2ca 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -269,10 +269,10 @@ extension ArgumentSetProvider { } extension ArgumentSet { - init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility) { + init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility, parent: InputKey.Parent) { #if DEBUG do { - try type._validate() + try type._validate(parent: parent) } catch { assertionFailure("\(error)") } @@ -281,22 +281,18 @@ extension ArgumentSet { let a: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child -> ArgumentSet? in - guard var codingKey = child.label else { return nil } + guard let codingKey = child.label else { return nil } if let parsed = child.value as? ArgumentSetProvider { guard parsed._visibility.isAtLeastAsVisible(as: visibility) else { return nil } - // Property wrappers have underscore-prefixed names - codingKey = String(codingKey.first == "_" - ? codingKey.dropFirst(1) - : codingKey.dropFirst(0)) - let key = InputKey(rawValue: codingKey) + let key = InputKey(name: codingKey, parent: parent) return parsed.argumentSet(for: key) } else { let arg = ArgumentDefinition( unparsedKey: codingKey, - default: nilOrValue(child.value)) + default: nilOrValue(child.value), parent: parent) // Save a non-wrapped property as is return ArgumentSet(arg) diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift index 98b341413..f633d91f7 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) -> ParsableArgumentsValidatorError? + static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? } enum ValidatorErrorKind { @@ -37,7 +37,7 @@ struct ParsableArgumentsValidationError: Error, CustomStringConvertible { } extension ParsableArguments { - static func _validate() throws { + static func _validate(parent: InputKey.Parent) throws { let validators: [ParsableArgumentsValidator.Type] = [ PositionalArgumentsValidator.self, ParsableArgumentsCodingKeyValidator.self, @@ -45,7 +45,7 @@ extension ParsableArguments { NonsenseFlagsValidator.self, ] let errors = validators.compactMap { validator in - validator.validate(self) + validator.validate(self, parent: parent) } if errors.count > 0 { throw ParsableArgumentsValidationError(parsableArgumentsType: self, underlayingErrors: errors) @@ -68,7 +68,6 @@ fileprivate extension ArgumentSet { /// in the argument list. Any other configuration leads to ambiguity in /// parsing the arguments. struct PositionalArgumentsValidator: ParsableArgumentsValidator { - struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { let repeatedPositionalArgument: String @@ -81,19 +80,16 @@ struct PositionalArgumentsValidator: ParsableArgumentsValidator { var kind: ValidatorErrorKind { .failure } } - static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { let sets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in guard - var codingKey = child.label, + let codingKey = child.label, let parsed = child.value as? ArgumentSetProvider else { return nil } - // Property wrappers have underscore-prefixed names - codingKey = String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) - - let key = InputKey(rawValue: codingKey) + let key = InputKey(name: codingKey, parent: parent) return parsed.argumentSet(for: key) } @@ -107,8 +103,8 @@ struct PositionalArgumentsValidator: ParsableArgumentsValidator { let firstRepeatedPositionalArgument: ArgumentDefinition = sets[repeatedPositional].firstRepeatedPositionalArgument! let positionalFollowingRepeatedArgument: ArgumentDefinition = positionalFollowingRepeated.firstPositionalArgument! return Error( - repeatedPositionalArgument: firstRepeatedPositionalArgument.help.keys.first!.rawValue, - positionalArgumentFollowingRepeated: positionalFollowingRepeatedArgument.help.keys.first!.rawValue) + repeatedPositionalArgument: firstRepeatedPositionalArgument.help.keys.first!.name, + positionalArgumentFollowingRepeated: positionalFollowingRepeatedArgument.help.keys.first!.name) } } @@ -116,11 +112,11 @@ struct PositionalArgumentsValidator: ParsableArgumentsValidator { struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { private struct Validator: Decoder { - let argumentKeys: [String] + let argumentKeys: [InputKey] enum ValidationResult: Swift.Error { case success - case missingCodingKeys([String]) + case missingCodingKeys([InputKey]) } let codingPath: [CodingKey] = [] @@ -135,7 +131,7 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { - let missingKeys = argumentKeys.filter { Key(stringValue: $0) == nil } + let missingKeys = argumentKeys.filter { Key(stringValue: $0.name) == nil } if missingKeys.isEmpty { throw ValidationResult.success } else { @@ -147,7 +143,7 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { /// This error indicates that an option, a flag, or an argument of /// a `ParsableArguments` is defined without a corresponding `CodingKey`. struct MissingKeysError: ParsableArgumentsValidatorError, CustomStringConvertible { - let missingCodingKeys: [String] + let missingCodingKeys: [InputKey] var description: String { let resolution = """ @@ -194,8 +190,8 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { } } - static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { - let argumentKeys: [String] = Mirror(reflecting: type.init()) + static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { + let argumentKeys: [InputKey] = Mirror(reflecting: type.init()) .children .compactMap { child in guard @@ -204,7 +200,7 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { else { return nil } // Property wrappers have underscore-prefixed names - return String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) + return InputKey(name: codingKey, parent: parent) } guard argumentKeys.count > 0 else { return nil @@ -239,19 +235,16 @@ struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator { var kind: ValidatorErrorKind { .failure } } - static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { let argSets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in guard - var codingKey = child.label, + let codingKey = child.label, let parsed = child.value as? ArgumentSetProvider else { return nil } - // Property wrappers have underscore-prefixed names - codingKey = String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) - - let key = InputKey(rawValue: codingKey) + let key = InputKey(name: codingKey, parent: parent) return parsed.argumentSet(for: key) } @@ -290,19 +283,16 @@ struct NonsenseFlagsValidator: ParsableArgumentsValidator { var kind: ValidatorErrorKind { .warning } } - static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + static func validate(_ type: ParsableArguments.Type, parent: InputKey.Parent) -> ParsableArgumentsValidatorError? { let argSets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in guard - var codingKey = child.label, + let codingKey = child.label, let parsed = child.value as? ArgumentSetProvider else { return nil } - // Property wrappers have underscore-prefixed names - codingKey = String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) - - let key = InputKey(rawValue: codingKey) + let key = InputKey(name: codingKey, parent: parent) return parsed.argumentSet(for: key) } diff --git a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift index c9ad4b47d..bdf211e0c 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @@ -160,7 +160,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).contains(where: { + ArgumentSet(self, visibility: .private, parent: .root).contains(where: { $0.isRepeatingPositional && $0.parsingStrategy == .allRemainingInput }) } diff --git a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift index 8784adb29..616785dad 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift @@ -33,11 +33,6 @@ final class ArgumentDecoder: Decoder { self.values = values self.previouslyDecoded = previouslyDecoded self.usedOrigins = InputOrigin() - - // Mark the terminator position(s) as used: - values.elements.values.filter { $0.key == .terminator }.forEach { - usedOrigins.formUnion($0.inputOrigin) - } } let values: ParsedValues @@ -95,7 +90,7 @@ final class ParsedArgumentsContainer: KeyedDecodingContainerProtocol where K } fileprivate func element(forKey key: K) -> ParsedValues.Element? { - let k = InputKey(key) + let k = InputKey(codingKey: key, path: codingPath) return decoder.element(forKey: k) } @@ -108,7 +103,7 @@ final class ParsedArgumentsContainer: KeyedDecodingContainerProtocol where K } func decode(_ type: T.Type, forKey key: K) throws -> T where T : Decodable { - let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(key), parsedElement: element(forKey: key)) + let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(codingKey: key, path: codingPath), parsedElement: element(forKey: key)) return try type.init(from: subDecoder) } @@ -117,7 +112,7 @@ final class ParsedArgumentsContainer: KeyedDecodingContainerProtocol where K if let parsedElement = parsedElement, parsedElement.inputOrigin.isDefaultValue { return parsedElement.value as? T } - let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(key), parsedElement: parsedElement) + let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(codingKey: key, path: codingPath), parsedElement: parsedElement) do { return try type.init(from: subDecoder) } catch let error as ParserError { @@ -159,7 +154,9 @@ struct SingleValueDecoder: Decoder { func unkeyedContainer() throws -> UnkeyedDecodingContainer { guard let e = parsedElement else { - throw ParserError.noValue(forKey: InputKey(rawValue: codingPath.last!.stringValue)) + var errorPath = codingPath + let last = errorPath.popLast()! + throw ParserError.noValue(forKey: InputKey(codingKey: last, path: errorPath)) } guard let a = e.value as? [Any] else { throw ParserError.invalidState @@ -192,7 +189,9 @@ struct SingleValueDecoder: Decoder { func decode(_ type: T.Type) throws -> T where T : Decodable { guard let e = parsedElement else { - throw ParserError.noValue(forKey: InputKey(rawValue: codingPath.last!.stringValue)) + var errorPath = codingPath + let last = errorPath.popLast()! + throw ParserError.noValue(forKey: InputKey(codingKey: last, path: errorPath)) } guard let s = e.value as? T else { throw InternalParseError.wrongType(e.value, forKey: e.key) diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index 9b83ff5ce..e8aebb7c2 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -108,7 +108,7 @@ struct ArgumentDefinition { var valueName: String { help.valueName.mapEmpty { names.preferredName?.valueString - ?? help.keys.first?.rawValue.convertedToSnakeCase(separator: "-") + ?? help.keys.first?.name.convertedToSnakeCase(separator: "-") ?? "value" } } @@ -209,10 +209,10 @@ 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?) { + init(unparsedKey: String, default defaultValue: Any?, parent: InputKey.Parent) { self.init( container: Bare.self, - key: InputKey(rawValue: unparsedKey), + key: InputKey(name: unparsedKey, parent: parent), kind: .default, allValues: [], help: .private, diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 3a3256a0f..1abaca160 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -434,7 +434,7 @@ extension ArgumentSet { func firstPositional( named name: String ) -> ArgumentDefinition? { - let key = InputKey(rawValue: name) + let key = InputKey(name: name, parent: .root) return first(where: { $0.help.keys.contains(key) }) } diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 1446f241b..ee327c41f 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) + let commandArguments = ArgumentSet(currentNode.element, visibility: .private, parent: .root) // 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) + let argset = ArgumentSet(current.element, visibility: .private, parent: .root) 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 new file mode 100644 index 000000000..567ab4bb1 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/InputKey.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// 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. +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 + + + /// Constructs a new ``InputKey``, cleaing the `name`, with the specified ``InputKey/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))) + } + + /// Constructs a new ``InputKey``, "cleaning the `value` and `path` if necessary. + /// + /// - Parameter codingKey: The base ``CodingKey`` + /// - Parameter path: The list of ``CodingKey`` values that lead to this one. May be empty. + @inlinable + init(codingKey: C, 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: C) -> String { + clean(codingKey: codingKey.stringValue) + } +} + +extension InputKey: CustomStringConvertible { + var description: String { + switch parent { + case .key(let parent): + return "\(parent).\(name)" + case .root: + return name + } + } +} diff --git a/Sources/ArgumentParser/Parsing/ParsedValues.swift b/Sources/ArgumentParser/Parsing/ParsedValues.swift index 69ef15cb5..7336b1e43 100644 --- a/Sources/ArgumentParser/Parsing/ParsedValues.swift +++ b/Sources/ArgumentParser/Parsing/ParsedValues.swift @@ -9,20 +9,6 @@ // //===----------------------------------------------------------------------===// -struct InputKey: RawRepresentable, Hashable { - var rawValue: String - - init(rawValue: String) { - self.rawValue = rawValue - } - - init(_ codingKey: C) { - self.rawValue = codingKey.stringValue - } - - static let terminator = InputKey(rawValue: "__terminator") -} - /// The resulting values after parsing the command-line arguments. /// /// This is a flat key-value list of values. diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 2f7c9c6c4..a7326f87a 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) }) + guard var arguments = self.last.map({ ArgumentSet($0, visibility: .private, parent: .root) }) 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 1bf3df3ca..97fa3a9e1 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -94,7 +94,7 @@ internal struct HelpGenerator { fatalError() } - let currentArgSet = ArgumentSet(currentCommand, visibility: visibility) + let currentArgSet = ArgumentSet(currentCommand, visibility: visibility, parent: .root) self.commandStack = commandStack // Build the tool name and subcommand name from the command configuration @@ -271,7 +271,7 @@ fileprivate extension NameSpecification { /// step, the name are returned in descending order. func generateHelpNames(visibility: ArgumentVisibility) -> [Name] { self - .makeNames(InputKey(rawValue: "help")) + .makeNames(InputKey(name: "help", parent: .root)) .compactMap { name in guard visibility.base != .default else { return name } switch name { @@ -312,7 +312,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type options: [.isOptional], help: "Show the version.", defaultValue: nil, - key: InputKey(rawValue: ""), + key: InputKey(name: "", parent: .root), isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) @@ -329,7 +329,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type options: [.isOptional], help: "Show help information.", defaultValue: nil, - key: InputKey(rawValue: ""), + key: InputKey(name: "", parent: .root), isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) @@ -344,7 +344,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type options: [.isOptional], help: ArgumentHelp("Dump help information as JSON."), defaultValue: nil, - key: InputKey(rawValue: ""), + key: InputKey(name: "", parent: .root), isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) @@ -354,7 +354,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) }) + guard var arguments = self.last.map({ ArgumentSet($0, visibility: visibility, parent: .root) }) 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 eff4c0aa3..2e95c96a9 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -121,7 +121,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) + let argumentSet = ArgumentSet(commandStack.last!, visibility: .default, parent: .root) 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 cbb1c7198..cb8b9e039 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -22,10 +22,10 @@ extension UsageGenerator { self.init(toolName: toolName, definition: definition) } - init(toolName: String, parsable: ParsableArguments, visibility: ArgumentVisibility) { + init(toolName: String, parsable: ParsableArguments, visibility: ArgumentVisibility, parent: InputKey.Parent) { self.init( toolName: toolName, - definition: ArgumentSet(type(of: parsable), visibility: visibility)) + definition: ArgumentSet(type(of: parsable), visibility: visibility, parent: parent)) } init(toolName: String, definition: [ArgumentSet]) { diff --git a/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift index 20b1d9d00..c47ac2776 100644 --- a/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift @@ -108,3 +108,56 @@ extension OptionGroupEndToEndTests { XCTAssertThrowsError(try Outer.parse(["prefix", "name", "postfix", "--size", "a"])) } } + +fileprivate struct DuplicatedFlagOption: ParsableArguments { + @Flag(name: .customLong("duplicated-option")) + var duplicated: Bool = false + + enum CodingKeys: CodingKey { + case duplicated + } +} + +fileprivate struct DuplicatedFlagCommand: ParsableCommand { + + @Flag + var duplicated: Bool = false + + @OptionGroup var option: DuplicatedFlagOption + + enum CodingKeys: CodingKey { + case duplicated + case option + } +} + +extension OptionGroupEndToEndTests { + func testUniqueNamesForDuplicatedFlag_NoFlags() throws { + AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.self, []) { command in + XCTAssertFalse(command.duplicated) + XCTAssertFalse(command.option.duplicated) + } + } + + func testUniqueNamesForDuplicatedFlag_RootOnly() throws { + AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.self, ["--duplicated"]) { command in + XCTAssertTrue(command.duplicated) + XCTAssertFalse(command.option.duplicated) + } + } + + func testUniqueNamesForDuplicatedFlag_OptionOnly() throws { + AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.self, ["--duplicated-option"]) { command in + XCTAssertFalse(command.duplicated) + XCTAssertTrue(command.option.duplicated) + } + } + + func testUniqueNamesForDuplicatedFlag_RootAndOption() throws { + AssertParseCommand(DuplicatedFlagCommand.self, DuplicatedFlagCommand.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 abd6c5929..84adf8423 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) + let opts = ArgumentSet(AllValues.self, visibility: .private, parent: .root) 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 956a4a83a..b0b6c905d 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(rawValue: "index") + let key = InputKey(name: "index", parent: .root) 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(rawValue: "index") + let key = InputKey(name: "index", parent: .root) 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,8 +42,8 @@ extension NameSpecificationTests { } } -fileprivate func Assert(nameSpecification: NameSpecification, key: String, makeNames expected: [Name], file: StaticString = #file, line: UInt = #line) { - let names = nameSpecification.makeNames(InputKey(rawValue: key)) +fileprivate func Assert(nameSpecification: NameSpecification, key: String, parent: InputKey.Parent = .root, 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 9f82d1191..cb2ba5f6e 100644 --- a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift +++ b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift @@ -81,29 +81,35 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testCodingKeyValidation() throws { - XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(A.self)) - XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(B.self)) + let parent = InputKey.Parent.key(InputKey(name: "parentKey", parent: .root)) + XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(A.self, parent: parent)) + XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(B.self, parent: parent)) - if let error = ParsableArgumentsCodingKeyValidator.validate(C.self) + if let error = ParsableArgumentsCodingKeyValidator.validate(C.self, parent: parent) as? ParsableArgumentsCodingKeyValidator.MissingKeysError { - XCTAssert(error.missingCodingKeys == ["count"]) + XCTAssert(error.missingCodingKeys == [InputKey(name: "count", parent: parent)]) } else { XCTFail() } - if let error = ParsableArgumentsCodingKeyValidator.validate(D.self) + if let error = ParsableArgumentsCodingKeyValidator.validate(D.self, parent: parent) as? ParsableArgumentsCodingKeyValidator.MissingKeysError { - XCTAssert(error.missingCodingKeys == ["phrase"]) + XCTAssert(error.missingCodingKeys == [ + InputKey(name: "phrase", parent: parent) + ]) } else { XCTFail() } - if let error = ParsableArgumentsCodingKeyValidator.validate(E.self) + if let error = ParsableArgumentsCodingKeyValidator.validate(E.self, parent: parent) as? ParsableArgumentsCodingKeyValidator.MissingKeysError { - XCTAssert(error.missingCodingKeys == ["phrase", "includeCounter"]) + XCTAssert(error.missingCodingKeys == [ + InputKey(name: "phrase", parent: parent), + InputKey(name: "includeCounter", parent: parent), + ]) } else { XCTFail() } @@ -124,7 +130,8 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testCustomDecoderValidation() throws { - if let error = ParsableArgumentsCodingKeyValidator.validate(TypeWithInvalidDecoder.self) + let parent = InputKey.Parent(InputKey(name: "foo", parent: .root)) + if let error = ParsableArgumentsCodingKeyValidator.validate(TypeWithInvalidDecoder.self, parent: parent) as? ParsableArgumentsCodingKeyValidator.InvalidDecoderError { XCTAssert(error.type == TypeWithInvalidDecoder.self) @@ -204,20 +211,21 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testPositionalArgumentsValidation() throws { - XCTAssertNil(PositionalArgumentsValidator.validate(A.self)) - XCTAssertNil(PositionalArgumentsValidator.validate(F.self)) - XCTAssertNil(PositionalArgumentsValidator.validate(H.self)) - XCTAssertNil(PositionalArgumentsValidator.validate(I.self)) - XCTAssertNil(PositionalArgumentsValidator.validate(K.self)) - - if let error = PositionalArgumentsValidator.validate(G.self) as? PositionalArgumentsValidator.Error { + let parent = InputKey.Parent(InputKey(name: "foo", parent: .root)) + XCTAssertNil(PositionalArgumentsValidator.validate(A.self, parent: parent)) + XCTAssertNil(PositionalArgumentsValidator.validate(F.self, parent: parent)) + XCTAssertNil(PositionalArgumentsValidator.validate(H.self, parent: parent)) + XCTAssertNil(PositionalArgumentsValidator.validate(I.self, parent: parent)) + XCTAssertNil(PositionalArgumentsValidator.validate(K.self, parent: parent)) + + if let error = PositionalArgumentsValidator.validate(G.self, parent: parent) as? PositionalArgumentsValidator.Error { XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") XCTAssert(error.repeatedPositionalArgument == "items") } else { XCTFail() } - if let error = PositionalArgumentsValidator.validate(J.self) as? PositionalArgumentsValidator.Error { + if let error = PositionalArgumentsValidator.validate(J.self, parent: parent) as? PositionalArgumentsValidator.Error { XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") XCTAssert(error.repeatedPositionalArgument == "numberOfItems") } else { @@ -238,7 +246,8 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_NoViolation() throws { - XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DifferentNames.self)) + let parent = InputKey.Parent(InputKey(name: "foo", parent: .root)) + XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DifferentNames.self, parent: parent)) } // MARK: One name is duplicated @@ -251,7 +260,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_TwoOfSameName() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(TwoOfTheSameName.self) + if let error = ParsableArgumentsUniqueNamesValidator.validate(TwoOfTheSameName.self, parent: .root) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"--foo\".") @@ -279,7 +288,8 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_TwoDuplications() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleUniquenessViolations.self) + let parent = InputKey.Parent(InputKey(name: "option", parent: .root)) + if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleUniquenessViolations.self, parent: parent) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssert( @@ -314,7 +324,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_ArgumentHasMultipleNames() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleNamesPerArgument.self) + if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleNamesPerArgument.self, parent: .root) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"-v\".") @@ -345,7 +355,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_MoreThanTwoDuplications() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(FourDuplicateNames.self) + if let error = ParsableArgumentsUniqueNamesValidator.validate(FourDuplicateNames.self, parent: .root) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (4) `Option` or `Flag` arguments are named \"--foo\".") @@ -387,7 +397,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_DuplicatedFlagFirstLetters_ShortNames() throws { - if let error = ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersShortNames.self) + if let error = ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersShortNames.self, parent: .root) as? ParsableArgumentsUniqueNamesValidator.Error { XCTAssertEqual(error.description, "Multiple (3) `Option` or `Flag` arguments are named \"-f\".") @@ -397,9 +407,9 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_DuplicatedFlagFirstLetters_LongNames() throws { - XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self)) + XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self, parent: .root)) } - + fileprivate struct HasOneNonsenseFlag: ParsableCommand { enum ExampleEnum: String, EnumerableFlag { case first @@ -429,7 +439,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testNonsenseFlagsValidation_OneFlag() throws { - if let error = NonsenseFlagsValidator.validate(HasOneNonsenseFlag.self) + if let error = NonsenseFlagsValidator.validate(HasOneNonsenseFlag.self, parent: .root) as? NonsenseFlagsValidator.Error { XCTAssertEqual( @@ -466,8 +476,8 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testNonsenseFlagsValidation_MultipleFlags() throws { - if let error = NonsenseFlagsValidator.validate(MultipleNonsenseFlags.self) - as? NonsenseFlagsValidator.Error + if let error = NonsenseFlagsValidator.validate(MultipleNonsenseFlags.self, parent: .root) + as? NonsenseFlagsValidator.Error { XCTAssertEqual( error.description, @@ -479,7 +489,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { To resolve this error, change the default to `false`, provide a value for the `inversion:` parameter, or remove the `@Flag` property wrapper altogether. - + Affected flag(s): --stuff --nonsense diff --git a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift index 218861fa1..7c65a809f 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) + let help = UsageGenerator(toolName: "example", parsable: T(), visibility: visibility, parent: .root) XCTAssertEqual(help.synopsis, expected, file: file, line: line) }