From 926834202e30a176b40b6ec92a35e5815b6da637 Mon Sep 17 00:00:00 2001 From: awaisdev Date: Wed, 21 Oct 2026 02:37:33 +0500 Subject: [PATCH] Fail on duplicate subcommand names or aliases --- .../Parsing/CommandParser.swift | 8 +++ Sources/ArgumentParser/Utilities/Tree.swift | 14 ++++- Tests/ArgumentParserUnitTests/TreeTests.swift | 60 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 46368b2f3..05be1dc75 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -60,6 +60,14 @@ struct CommandParser { The command \"\(command)\" can't have an alias with the same name \ as the command itself. """.wrapped(to: 70)) + } catch Tree.InitializationError + .duplicateSubcommand(let command) + { + configurationFailure( + """ + The command \"\(command)\" can't be used as a subcommand more than \ + once. + """.wrapped(to: 70)) } catch { fatalError("Unexpected error: \(error).") } diff --git a/Sources/ArgumentParser/Utilities/Tree.swift b/Sources/ArgumentParser/Utilities/Tree.swift index 73e953909..6079b78b9 100644 --- a/Sources/ArgumentParser/Utilities/Tree.swift +++ b/Sources/ArgumentParser/Utilities/Tree.swift @@ -93,14 +93,25 @@ extension Tree where Element == ParsableCommand.Type { convenience init(root command: ParsableCommand.Type) throws { self.init(command) + var subcommandNames: Set = [] for subcommand in command.configuration.subcommands { if subcommand == command { throw InitializationError.recursiveSubcommand(subcommand) } // We don't allow an alias that has the same name as the command itself. - if subcommand.configuration.aliases.contains(subcommand._commandName) { + let subcommandName = subcommand._commandName + if subcommand.configuration.aliases.contains(subcommandName) { throw InitializationError.aliasMatchingCommand(subcommand) } + // Subcommands must have unique names and aliases so that each name + // unambiguously selects a single subcommand. + guard subcommandNames.insert(subcommandName).inserted else { + throw InitializationError.duplicateSubcommand(subcommand) + } + for alias in subcommand.configuration.aliases + where !subcommandNames.insert(alias).inserted { + throw InitializationError.duplicateSubcommand(subcommand) + } try addChild(Tree(root: subcommand)) } } @@ -108,5 +119,6 @@ extension Tree where Element == ParsableCommand.Type { enum InitializationError: Error { case recursiveSubcommand(ParsableCommand.Type) case aliasMatchingCommand(ParsableCommand.Type) + case duplicateSubcommand(ParsableCommand.Type) } } diff --git a/Tests/ArgumentParserUnitTests/TreeTests.swift b/Tests/ArgumentParserUnitTests/TreeTests.swift index fade80218..93f1ac99e 100644 --- a/Tests/ArgumentParserUnitTests/TreeTests.swift +++ b/Tests/ArgumentParserUnitTests/TreeTests.swift @@ -86,6 +86,48 @@ extension TreeTests { } } + struct RootWithDuplicateSubcommands: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [ + FirstDuplicate.self, SecondDuplicate.self + ]) + + struct FirstDuplicate: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "duplicate") + } + struct SecondDuplicate: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "duplicate") + } + } + + struct RootWithAliasedSubcommands: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [ + Original.self, Other.self + ]) + + struct Original: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "original", aliases: ["shared"]) + } + struct Other: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "other", aliases: ["shared"]) + } + } + + struct RootWithDistinctSubcommands: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [ + Unique.self, Aliased.self + ]) + + struct Unique: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "unique") + } + struct Aliased: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "aliased", aliases: ["alias"]) + } + } + @Test func initializationWithRecursiveSubcommand() { #expect(throws: (any Error).self) { try Tree(root: A.asCommand) } #expect(throws: (any Error).self) { try Tree(root: Root.asCommand) } @@ -99,4 +141,22 @@ extension TreeTests { try Tree(root: RootWithNestedSub.asCommand) } } + + @Test func initializationWithDuplicateSubcommands() { + #expect(throws: (any Error).self) { + try Tree(root: RootWithDuplicateSubcommands.asCommand) + } + } + + @Test func initializationWithConflictingAliases() { + #expect(throws: (any Error).self) { + try Tree(root: RootWithAliasedSubcommands.asCommand) + } + } + + @Test func initializationWithDistinctSubcommandsSucceeds() { + #expect(throws: Never.self) { + try Tree(root: RootWithDistinctSubcommands.asCommand) + } + } }