Files
swift-argument-parser/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift
Nate Cook a9b9644153 Stop removing underscores from CodingKey names in InputKey (#548)
When a property wrapper is applied to a property, the property's
storage is given a name with a prefixed underscore. That is,
for a property named `x`, the actual storage is named `_x`.

That prefixed storage is what is visible through reflection, so
when building an ArgumentSet from a command type's Mirror, we
need to remove the leading underscore. This is done when creating
an InputKey for each property.

However, InputKeys are also created from CodingKeys during
decoding of a ParsableCommand. These CodingKeys _do not_ have
the leading underscore that is visible, so any underscores
that appear are actually from the declaration of the property
with an underscored name. Removing leading underscores from
CodingKey names results in a mismatch when trying to find
the decoded value.

This change simplifies the InputKey type to use an array
path instead of an indirect enum and removes the leading
underscore dropping when creating an InputKey from a CodingKey.

rdar://104928743
2023-02-02 08:33:51 -06:00

172 lines
5.6 KiB
Swift

//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2020 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
//
//===----------------------------------------------------------------------===//
@_implementationOnly import ArgumentParserToolInfo
@_implementationOnly import class Foundation.JSONEncoder
internal struct DumpHelpGenerator {
var toolInfo: ToolInfoV0
init(_ type: ParsableArguments.Type) {
self.init(commandStack: [type.asCommand])
}
init(commandStack: [ParsableCommand.Type]) {
self.toolInfo = ToolInfoV0(commandStack: commandStack)
}
func rendered() -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if #available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) {
encoder.outputFormatting.insert(.sortedKeys)
}
guard let encoded = try? encoder.encode(self.toolInfo) else { return "" }
return String(data: encoded, encoding: .utf8) ?? ""
}
}
fileprivate extension BidirectionalCollection where Element == ParsableCommand.Type {
/// 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: nil) })
else { return ArgumentSet() }
self.versionArgumentDefinition().map { arguments.append($0) }
self.helpArgumentDefinition().map { arguments.append($0) }
return arguments
}
}
fileprivate extension ArgumentSet {
func mergingCompositeArguments() -> ArgumentSet {
var arguments = ArgumentSet()
var slice = self[...]
while var argument = slice.popFirst() {
if argument.help.isComposite {
// If this argument is composite, we have a group of arguments to
// merge together.
let groupEnd = slice
.firstIndex { $0.help.keys != argument.help.keys }
?? slice.endIndex
let group = [argument] + slice[..<groupEnd]
slice = slice[groupEnd...]
switch argument.kind {
case .named:
argument.kind = .named(group.flatMap(\.names))
case .positional, .default:
break
}
argument.help.valueName = group.map(\.valueName).first { !$0.isEmpty } ?? ""
argument.help.defaultValue = group.compactMap(\.help.defaultValue).first
argument.help.abstract = group.map(\.help.abstract).first { !$0.isEmpty } ?? ""
argument.help.discussion = group.map(\.help.discussion).first { !$0.isEmpty } ?? ""
}
arguments.append(argument)
}
return arguments
}
}
fileprivate extension ToolInfoV0 {
init(commandStack: [ParsableCommand.Type]) {
self.init(command: CommandInfoV0(commandStack: commandStack))
}
}
fileprivate extension CommandInfoV0 {
init(commandStack: [ParsableCommand.Type]) {
guard let command = commandStack.last else {
preconditionFailure("commandStack must not be empty")
}
let parents = commandStack.dropLast()
var superCommands = parents.map { $0._commandName }
if let superName = parents.first?.configuration._superCommandName {
superCommands.insert(superName, at: 0)
}
let defaultSubcommand = command.configuration.defaultSubcommand?
.configuration.commandName
let subcommands = command.configuration.subcommands
.map { subcommand -> CommandInfoV0 in
var commandStack = commandStack
commandStack.append(subcommand)
return CommandInfoV0(commandStack: commandStack)
}
let arguments = commandStack
.allArguments()
.mergingCompositeArguments()
.compactMap(ArgumentInfoV0.init)
self = CommandInfoV0(
superCommands: superCommands,
commandName: command._commandName,
abstract: command.configuration.abstract,
discussion: command.configuration.discussion,
defaultSubcommand: defaultSubcommand,
subcommands: subcommands,
arguments: arguments)
}
}
fileprivate extension ArgumentInfoV0 {
init?(argument: ArgumentDefinition) {
guard let kind = ArgumentInfoV0.KindV0(argument: argument) else { return nil }
self.init(
kind: kind,
shouldDisplay: argument.help.visibility.base == .default,
sectionTitle: argument.help.parentTitle.nonEmpty,
isOptional: argument.help.options.contains(.isOptional),
isRepeating: argument.help.options.contains(.isRepeating),
names: argument.names.map(ArgumentInfoV0.NameInfoV0.init),
preferredName: argument.names.preferredName.map(ArgumentInfoV0.NameInfoV0.init),
valueName: argument.valueName,
defaultValue: argument.help.defaultValue,
allValues: argument.help.allValues,
abstract: argument.help.abstract,
discussion: argument.help.discussion)
}
}
fileprivate extension ArgumentInfoV0.KindV0 {
init?(argument: ArgumentDefinition) {
switch argument.kind {
case .named:
switch argument.update {
case .nullary:
self = .flag
case .unary:
self = .option
}
case .positional:
self = .positional
case .default:
return nil
}
}
}
fileprivate extension ArgumentInfoV0.NameInfoV0 {
init(name: Name) {
switch name {
case let .long(n):
self.init(kind: .long, name: n)
case let .short(n, _):
self.init(kind: .short, name: String(n))
case let .longWithSingleDash(n):
self.init(kind: .longWithSingleDash, name: n)
}
}
}