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

Adopt ArgumentParser for the command-line tool #154

Merged
merged 5 commits into from
Mar 10, 2020
Merged
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
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
"state": {
"branch": null,
"revision": "35b76bf577d3cc74820f8991894ce3bcdf024ddc",
"version": "0.0.2"
}
},
{
"package": "SwiftSyntax",
"repositoryURL": "https://github.com/apple/swift-syntax",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ let package = Package(
.revision("swift-DEVELOPMENT-SNAPSHOT-2020-01-29-a")
),
.package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.0.1"),
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.2")),
],
targets: [
.target(
Expand Down Expand Up @@ -69,6 +70,7 @@ let package = Package(
"SwiftFormatCore",
"SwiftSyntax",
"SwiftToolsSupport-auto",
"ArgumentParser",
]
),
.testTarget(
Expand Down
208 changes: 66 additions & 142 deletions Sources/swift-format/CommandLineOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,191 +10,115 @@
//
//===----------------------------------------------------------------------===//

import ArgumentParser
import Foundation
import SwiftFormat
import TSCBasic
import TSCUtility

/// Collects the command line options that were passed to `swift-format`.
struct CommandLineOptions {

struct SwiftFormatCommand: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "swift-format",
abstract: "Format or lint Swift source code.",
discussion: "When no files are specified, it expects the source from standard input."
)

/// The path to the JSON configuration file that should be loaded.
///
/// If not specified, the default configuration will be used.
var configurationPath: String? = nil
@Option(
name: .customLong("configuration"),
help: "The path to a JSON file containing the configuration of the linter/formatter.")
var configurationPath: String?

/// The filename for the source code when reading from standard input, to include in diagnostic
/// messages.
///
/// If not specified and standard input is used, a dummy filename is used for diagnostic messages
/// about the source from standard input.
var assumeFilename: String? = nil
@Option(help: "When using standard input, the filename of the source to include in diagnostics.")
var assumeFilename: String?

enum ToolMode: String, CaseIterable, ExpressibleByArgument {
case format
case lint
case dumpConfiguration = "dump-configuration"
}

/// The mode in which to run the tool.
///
/// If not specified, the tool will be run in format mode.
var mode: ToolMode = .format
@Option(
default: .format,
help: "The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'.")
var mode: ToolMode

/// Whether or not to format the Swift file in-place
///
/// If specified, the current file is overwritten when formatting
var inPlace: Bool = false
@Flag(
name: .shortAndLong,
help: "Overwrite the current file when formatting ('format' mode only).")
var inPlace: Bool

/// Whether or not to run the formatter/linter recursively.
///
/// If set, we recursively run on all ".swift" files in any provided directories.
var recursive: Bool = false
@Flag(
name: .shortAndLong,
help: "Recursively run on '.swift' files in any provided directories.")
var recursive: Bool

/// The list of paths to Swift source files that should be formatted or linted.
@Argument(help: "One or more input filenames")
var paths: [String]

@Flag(help: "Print the version and exit")
var version: Bool

@Flag(help: .hidden) var debugDisablePrettyPrint: Bool
@Flag(help: .hidden) var debugDumpTokenStream: Bool

/// Advanced options that are useful for developing/debugging but otherwise not meant for general
/// use.
var debugOptions: DebugOptions = []

/// The list of paths to Swift source files that should be formatted or linted.
var paths: [String] = []
}

/// Process the command line argument strings and returns an object containing their values.
///
/// - Parameters:
/// - commandName: The name of the command that this tool was invoked as.
/// - arguments: The remaining command line arguments after the command name.
/// - Returns: A `CommandLineOptions` value that contains the parsed options.
func processArguments(commandName: String, _ arguments: [String]) -> CommandLineOptions {
let parser = ArgumentParser(
commandName: commandName,
usage: "[options] [filename or path ...]",
overview:
"""
Format or lint Swift source code.

When no files are specified, it expects the source from standard input.
"""
)

let binder = ArgumentBinder<CommandLineOptions>()
binder.bind(
option: parser.add(
option: "--mode",
shortName: "-m",
kind: ToolMode.self,
usage: "The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'."
)
) {
$0.mode = $1
}
binder.bind(
option: parser.add(
option: "--version",
shortName: "-v",
kind: Bool.self,
usage: "Prints the version and exists"
)
) { opts, _ in
opts.mode = .version
}
binder.bindArray(
positional: parser.add(
positional: "filenames or paths",
kind: [String].self,
optional: true,
strategy: .upToNextOption,
usage: "One or more input filenames",
completion: .filename
)
) {
$0.paths = $1
}
binder.bind(
option: parser.add(
option: "--configuration",
kind: String.self,
usage: "The path to a JSON file containing the configuration of the linter/formatter."
)
) {
$0.configurationPath = $1
}
binder.bind(
option: parser.add(
option: "--assume-filename",
kind: String.self,
usage: "When using standard input, the filename of the source to include in diagnostics."
)
) {
$0.assumeFilename = $1
}
binder.bind(
option: parser.add(
option: "--in-place",
shortName: "-i",
kind: Bool.self,
usage: "Overwrite the current file when formatting ('format' mode only)."
)
) {
$0.inPlace = $1
}
binder.bind(
option: parser.add(
option: "--recursive",
shortName: "-r",
kind: Bool.self,
usage: "Recursively run on '.swift' files in any provided directories."
)
) {
$0.recursive = $1
var debugOptions: DebugOptions {
[
debugDisablePrettyPrint ? .disablePrettyPrint : [],
debugDumpTokenStream ? .dumpTokenStream : [],
]
}

// Add advanced debug/developer options. These intentionally have no usage strings, which omits
// them from the `--help` screen to avoid noise for the general user.
binder.bind(
option: parser.add(
option: "--debug-disable-pretty-print",
kind: Bool.self
)
) {
$0.debugOptions.set(.disablePrettyPrint, enabled: $1)
}
binder.bind(
option: parser.add(
option: "--debug-dump-token-stream",
kind: Bool.self
)
) {
$0.debugOptions.set(.dumpTokenStream, enabled: $1)
}

var opts = CommandLineOptions()
do {
let args = try parser.parse(arguments)
try binder.fill(parseResult: args, into: &opts)

if opts.inPlace && (ToolMode.format != opts.mode || opts.paths.isEmpty) {
throw ArgumentParserError.unexpectedArgument("--in-place, -i")
mutating func validate() throws {
if version {
throw CleanExit.message("0.0.1")
}

if inPlace && (mode != .format || paths.isEmpty) {
throw ValidationError("'--in-place' is only valid when formatting files")
}

let modeSupportsRecursive = ToolMode.format == opts.mode || ToolMode.lint == opts.mode
if opts.recursive && (!modeSupportsRecursive || opts.paths.isEmpty) {
throw ArgumentParserError.unexpectedArgument("--recursive, -r")
let modeSupportsRecursive = mode == .format || mode == .lint
if recursive && (!modeSupportsRecursive || paths.isEmpty) {
throw ValidationError("'--recursive' is only valid when formatting or linting files")
}

if opts.assumeFilename != nil && !opts.paths.isEmpty {
throw ArgumentParserError.unexpectedArgument("--assume-filename")
if assumeFilename != nil && !paths.isEmpty {
throw ValidationError("'--assume-filename' is only valid when reading from stdin")
}

if !opts.paths.isEmpty && !opts.recursive {
for path in opts.paths {
if !paths.isEmpty && !recursive {
for path in paths {
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir), isDir.boolValue {
throw ArgumentParserError.invalidValue(
argument: "'\(path)'",
error: ArgumentConversionError.custom("for directories, use --recursive option")
throw ValidationError(
"""
'\(path)' is a path to a directory, not a Swift source file.
Use the '--recursive' option to handle directories.
"""
)
}
}
}
} catch {
stderrStream.write("error: \(error)\n\n")
parser.printUsage(on: stderrStream)
exit(1)
}
return opts
}
13 changes: 13 additions & 0 deletions Sources/swift-format/FormatError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation
import SwiftSyntax

struct FormatError: LocalizedError {
var message: String
var errorDescription: String? { message }

static var exitWithDiagnosticErrors: FormatError {
// The diagnostics engine has already printed errors to stderr.
FormatError(message: "")
}
}

22 changes: 10 additions & 12 deletions Sources/swift-format/Run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ import TSCBasic
func lintMain(
configuration: Configuration, sourceFile: FileHandle, assumingFilename: String?,
debugOptions: DebugOptions, diagnosticEngine: DiagnosticEngine
) -> Int {
) {
let linter = SwiftLinter(configuration: configuration, diagnosticEngine: diagnosticEngine)
linter.debugOptions = debugOptions
let assumingFileURL = URL(fileURLWithPath: assumingFilename ?? "<stdin>")

guard let source = readSource(from: sourceFile) else {
diagnosticEngine.diagnose(
Diagnostic.Message(.error, "Unable to read source for linting from \(assumingFileURL.path)."))
return 1
return
}

do {
Expand All @@ -47,20 +47,19 @@ func lintMain(
let path = assumingFileURL.path
diagnosticEngine.diagnose(
Diagnostic.Message(.error, "Unable to lint \(path): file is not readable or does not exist."))
return 1
return
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
let path = assumingFileURL.path
let location = SourceLocationConverter(file: path, source: source).location(for: position)
diagnosticEngine.diagnose(
Diagnostic.Message(.error, "file contains invalid or unrecognized Swift syntax."),
location: location)
return 1
return
} catch {
let path = assumingFileURL.path
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to lint \(path): \(error)"))
exit(1)
return
}
return diagnosticEngine.diagnostics.isEmpty ? 0 : 1
}

/// Runs the formatting pipeline over the provided source file.
Expand All @@ -76,7 +75,7 @@ func lintMain(
func formatMain(
configuration: Configuration, sourceFile: FileHandle, assumingFilename: String?, inPlace: Bool,
debugOptions: DebugOptions, diagnosticEngine: DiagnosticEngine
) -> Int {
) {
// Even though `diagnosticEngine` is defined, it's use is reserved for fatal messages. Pass nil
// to the formatter to suppress other messages since they will be fixed or can't be automatically
// fixed anyway.
Expand All @@ -88,7 +87,7 @@ func formatMain(
diagnosticEngine.diagnose(
Diagnostic.Message(
.error, "Unable to read source for formatting from \(assumingFileURL.path)."))
return 1
return
}

do {
Expand All @@ -110,20 +109,19 @@ func formatMain(
diagnosticEngine.diagnose(
Diagnostic.Message(
.error, "Unable to format \(path): file is not readable or does not exist."))
return 1
return
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
let path = assumingFileURL.path
let location = SourceLocationConverter(file: path, source: source).location(for: position)
diagnosticEngine.diagnose(
Diagnostic.Message(.error, "file contains invalid or unrecognized Swift syntax."),
location: location)
return 1
return
} catch {
let path = assumingFileURL.path
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to format \(path): \(error)"))
exit(1)
return
}
return 0
}

/// Reads from the given file handle until EOF is reached, then returns the contents as a UTF8
Expand Down
Loading