Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Subcommand Autodiscovery #319

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 1 addition & 7 deletions Examples/math/main.swift
Expand Up @@ -21,11 +21,6 @@ struct Math: ParsableCommand {
// Commands can define a version for automatic '--version' support.
version: "1.0.0",

// Pass an array to `subcommands` to set up a nested tree of subcommands.
// With language support for type-level introspection, this could be
// provided by automatically finding nested `ParsableCommand` types.
subcommands: [Add.self, Multiply.self, Statistics.self],

// A default subcommand, when provided, is automatically selected if a
// subcommand is not given on the command line.
defaultSubcommand: Add.self)
Expand Down Expand Up @@ -82,8 +77,7 @@ extension Math {
// Command names are automatically generated from the type name
// by default; you can specify an override here.
commandName: "stats",
abstract: "Calculate descriptive statistics.",
subcommands: [Average.self, StandardDeviation.self, Quantiles.self])
abstract: "Calculate descriptive statistics.")
}
}

Expand Down
8 changes: 7 additions & 1 deletion Package.swift
Expand Up @@ -22,8 +22,14 @@ var package = Package(
dependencies: [],
targets: [
.target(
name: "ArgumentParser",
name: "_CRuntime",
dependencies: []),
.target(
name: "_Runtime",
dependencies: ["_CRuntime"]),
.target(
name: "ArgumentParser",
dependencies: ["_Runtime"]),
.target(
name: "ArgumentParserTestHelpers",
dependencies: ["ArgumentParser"]),
Expand Down
Expand Up @@ -34,7 +34,7 @@ struct BashCompletionsGenerator {
let subcommandArgument = isRootCommand ? "2" : "$(($1+1))"

// Include 'help' in the list of subcommands for the root command.
var subcommands = type.configuration.subcommands
var subcommands = type.subcommands
if !subcommands.isEmpty && isRootCommand {
subcommands.append(HelpCommand.self)
}
Expand Down
Expand Up @@ -29,7 +29,7 @@ struct FishCompletionsGenerator {
let type = commands.last!
let isRootCommand = commands.count == 1
let programName = commandChain[0]
var subcommands = type.configuration.subcommands
var subcommands = type.subcommands

if !subcommands.isEmpty {
if isRootCommand {
Expand Down
Expand Up @@ -37,7 +37,7 @@ struct ZshCompletionsGenerator {

var args = generateCompletionArguments(commands)

var subcommands = type.configuration.subcommands
var subcommands = type.subcommands
var subcommandHandler = ""
if !subcommands.isEmpty {
args.append("'(-): :->command'")
Expand Down
Expand Up @@ -39,7 +39,10 @@ public struct CommandConfiguration {
public var shouldDisplay: Bool

/// An array of the types that define subcommands for this command.
public var subcommands: [ParsableCommand.Type]
///
/// If `nil`, the argument parser will automatically discover nested
/// types who conform to `ParsableCommand` and use them as subcommands.
public var subcommands: [ParsableCommand.Type]?

/// The default command type to run if no subcommand is given.
public var defaultSubcommand: ParsableCommand.Type?
Expand Down Expand Up @@ -74,7 +77,7 @@ public struct CommandConfiguration {
discussion: String = "",
version: String = "",
shouldDisplay: Bool = true,
subcommands: [ParsableCommand.Type] = [],
subcommands: [ParsableCommand.Type]? = nil,
defaultSubcommand: ParsableCommand.Type? = nil,
helpNames: NameSpecification? = nil
) {
Expand All @@ -97,7 +100,7 @@ public struct CommandConfiguration {
discussion: String = "",
version: String = "",
shouldDisplay: Bool = true,
subcommands: [ParsableCommand.Type] = [],
subcommands: [ParsableCommand.Type]? = nil,
defaultSubcommand: ParsableCommand.Type? = nil,
helpNames: NameSpecification? = nil
) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/ArgumentParser/Parsable Types/ParsableCommand.swift
Expand Up @@ -44,6 +44,10 @@ extension ParsableCommand {
public mutating func run() throws {
throw CleanExit.helpRequest(self)
}

static var subcommands: [ParsableCommand.Type] {
configuration.subcommands ?? discoverSubcommands(for: Self.self)
}
}

// MARK: - API
Expand Down
7 changes: 4 additions & 3 deletions Sources/ArgumentParser/Usage/HelpGenerator.swift
Expand Up @@ -117,7 +117,7 @@ internal struct HelpGenerator {
}

var usageString = UsageGenerator(toolName: toolName, definition: [currentArgSet]).synopsis
if !currentCommand.configuration.subcommands.isEmpty {
if !currentCommand.subcommands.isEmpty {
if usageString.last != " " { usageString += " " }
usageString += "<subcommand>"
}
Expand Down Expand Up @@ -202,9 +202,10 @@ internal struct HelpGenerator {
}
}

let configuration = commandStack.last!.configuration
let command = commandStack.last!
let configuration = command.configuration
let subcommandElements: [Section.Element] =
configuration.subcommands.compactMap { command in
command.subcommands.compactMap { command in
guard command.configuration.shouldDisplay else { return nil }
var label = command._commandName
if command == configuration.defaultSubcommand {
Expand Down
106 changes: 106 additions & 0 deletions Sources/ArgumentParser/Utilities/SubcommandDiscovery.swift
@@ -0,0 +1,106 @@
//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2021 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
//
//===----------------------------------------------------------------------===//

import _Runtime
@_implementationOnly import Foundation

let subcommandLock = NSLock()
var discoveredSubcommands: [UnsafeRawPointer: [ParsableCommand.Type]] = [:]

// Use runtime metadata information to find nested subcommands automatically.
// When getting the subcommands for a ParsableCommand, we look at all types who
// conform to ParsableCommand within the same module. If we find one who
// is nested in the base command that we're looking at now, we add it as a
// subcommand.
//
// struct Base: ParsableCommand {}
//
// extension Base {
// // This is considered an automatic subcommand!
// struct Sub: ParsableCommand {}
// }
//
func discoverSubcommands(for type: Any.Type) -> [ParsableCommand.Type] {
// Make sure that only classes, structs, and enums are checked for.
guard let selfMetadata = metadata(for: type),
selfMetadata.kind != .existential else {
return []
}

subcommandLock.lock()

defer {
subcommandLock.unlock()
}

guard !discoveredSubcommands.keys.contains(selfMetadata.pointer) else {
return discoveredSubcommands[selfMetadata.pointer]!
}

let module = getModuleDescriptor(from: selfMetadata.descriptor)

let parsableCommand: ContextDescriptor

// If the swift_getExistentialTypeConstraints function is available, use that.
// Otherwise, fallback to using an earlier existential metadata layout.
if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *) {
// FIXME: Implement this function
fatalError("swift_getExistentialTypeConstraints not implemented")
} else {
let parsableCommandMetadata = metadata(
for: ParsableCommand.self
) as! ExistentialMetadata

parsableCommand = parsableCommandMetadata.protocols[0]
}

// Grab all of the conformances to ParsableCommand in the same module that
// this ParsableCommand was defined in.
let conformances = _Runtime.getConformances(for: parsableCommand, in: module)

// FIXME: Maybe we want to reserve some initial space here?
Azoy marked this conversation as resolved.
Show resolved Hide resolved
var subcommands: [ParsableCommand.Type] = []

for conformance in conformances {
// If we don't have a context descriptor, then an ObjC class somehow
// conformed to a Swift protocol (not sure that's possible).
guard let descriptor = conformance.contextDescriptor else {
continue
}

// This is okay because modules can't conform to protocols, so the type
// being referenced here is at least a child deep in the declaration context
// tree.
let parent = descriptor.parent!

// We're only interested in conformances where the parent is ourselves
// (the parent ParsableCommand).
guard parent == selfMetadata.descriptor else {
continue
}

// If a subcommand is generic, we can't add it as a default because we have
// no idea what type substitution they want for the generic parameter.
guard !descriptor.flags.isGeneric else {
continue
}

// We found a subcommand! Use the access function to get the metadata for
// it and add it to the list!
let subcommand = descriptor.accessor() as! ParsableCommand.Type
subcommands.append(subcommand)
}

// Remember to cache the results!
discoveredSubcommands[selfMetadata.pointer] = subcommands

return subcommands
}
2 changes: 1 addition & 1 deletion Sources/ArgumentParser/Utilities/Tree.swift
Expand Up @@ -90,7 +90,7 @@ extension Tree where Element == ParsableCommand.Type {

convenience init(root command: ParsableCommand.Type) throws {
self.init(command)
for subcommand in command.configuration.subcommands {
for subcommand in command.subcommands {
if subcommand == command {
throw InitializationError.recursiveSubcommand(subcommand)
}
Expand Down
68 changes: 68 additions & 0 deletions Sources/_CRuntime/ImageInspection.c
@@ -0,0 +1,68 @@
//===--------------------------------------------------------------*- C -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2021 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
//
//===----------------------------------------------------------------------===//

#include "ImageInspection.h"

//===----------------------------------------------------------------------===//
// MachO Image Inspection
//===----------------------------------------------------------------------===//

#if defined(__MACH__)

#include <mach-o/dyld.h>

void _loadImageCallback(const struct mach_header *header, intptr_t size) {
lookupSection(header, "__TEXT", "__swift5_proto",
registerProtocolConformances);
}

__attribute__((__constructor__))
void loadImages() {
_dyld_register_func_for_add_image(_loadImageCallback);
}

#endif // defined(__MACH__)

//===----------------------------------------------------------------------===//
// ELF Image Inspection
//===----------------------------------------------------------------------===//

#if defined(__ELF__)

#define SWIFT_REGISTER_SECTION(name, handle) \
handle(&__start_##name, &__stop_##name - &__start_##name);

__attribute__((__constructor__))
void loadImages() {
SWIFT_REGISTER_SECTION(swift5_protocol_conformances,
registerProtocolConformances)
}

#undef SWIFT_REGISTER_SECTION

#endif // defined(__ELF__)

//===----------------------------------------------------------------------===//
// COFF Image Inspection
//===----------------------------------------------------------------------===//

#if !defined(__MACH__) && !defined(__ELF__)

#define SWIFT_REGISTER_SECTION(name, handle) \
handle((const char *)&__start_##name, &__stop_##name - &__start_##name);

void loadImages() {
SWIFT_REGISTER_SECTION(sw5prtc, registerProtocolConformances)
}

#undef SWIFT_REGISTER_SECTION

#endif // !defined(__MACH__) && !defined(__ELF__)
12 changes: 12 additions & 0 deletions Sources/_CRuntime/MetadataViews.c
@@ -0,0 +1,12 @@
//===--------------------------------------------------------------*- C -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2021 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
//
//===----------------------------------------------------------------------===//

#include "MetadataViews.h"
21 changes: 21 additions & 0 deletions Sources/_CRuntime/PtrauthStrip.c
@@ -0,0 +1,21 @@
//===--------------------------------------------------------------*- C -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2021 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
//
//===----------------------------------------------------------------------===//

#include "PtrauthStrip.h"

#if defined(__arm64e__)
#include <ptrauth.h>

const void *__ptrauth_strip_asda(const void *pointer) {
return ptrauth_strip(pointer, ptrauth_key_asda);
}

#endif