Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Sources/ArgumentParser/Parsing/CommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParsableCommand.Type>.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).")
}
Expand Down
14 changes: 13 additions & 1 deletion Sources/ArgumentParser/Utilities/Tree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,32 @@ extension Tree where Element == ParsableCommand.Type {

convenience init(root command: ParsableCommand.Type) throws {
self.init(command)
var subcommandNames: Set<String> = []
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))
}
}

enum InitializationError: Error {
case recursiveSubcommand(ParsableCommand.Type)
case aliasMatchingCommand(ParsableCommand.Type)
case duplicateSubcommand(ParsableCommand.Type)
}
}
60 changes: 60 additions & 0 deletions Tests/ArgumentParserUnitTests/TreeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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)
}
}
}
Loading