Skip to content

Commit bbb2637

Browse files
authored
Merge pull request swiftlang#161 from allevato/subcommands
Migrate modes to subcommands.
2 parents 5d38f55 + c016d2d commit bbb2637

13 files changed

+450
-263
lines changed

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ let package = Package(
2626
.revision("swift-DEVELOPMENT-SNAPSHOT-2020-01-29-a")
2727
),
2828
.package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.0.1"),
29-
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.2")),
29+
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.4")),
3030
],
3131
targets: [
3232
.target(

Sources/swift-format/FormatError.swift

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import Foundation
15+
import SwiftFormatConfiguration
16+
import TSCBasic
17+
import TSCUtility
18+
19+
extension SwiftFormatCommand {
20+
/// Dumps the tool's default configuration in JSON format to standard output.
21+
struct DumpConfiguration: ParsableCommand {
22+
static var configuration = CommandConfiguration(
23+
abstract: "Dump the default configuration in JSON format to standard output")
24+
25+
func run() throws {
26+
let configuration = Configuration()
27+
do {
28+
let encoder = JSONEncoder()
29+
encoder.outputFormatting = [.prettyPrinted]
30+
if #available(macOS 10.13, *) {
31+
encoder.outputFormatting.insert(.sortedKeys)
32+
}
33+
34+
let data = try encoder.encode(configuration)
35+
guard let jsonString = String(data: data, encoding: .utf8) else {
36+
// This should never happen, but let's make sure we fail more gracefully than crashing, just
37+
// in case.
38+
throw FormatError(
39+
message: "Could not dump the default configuration: the JSON was not valid UTF-8")
40+
}
41+
print(jsonString)
42+
} catch {
43+
throw FormatError(message: "Could not dump the default configuration: \(error)")
44+
}
45+
}
46+
}
47+
}

Sources/swift-format/Run.swift renamed to Sources/swift-format/Subcommands/Format.swift

Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,70 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
99
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import ArgumentParser
1314
import Foundation
1415
import SwiftFormat
1516
import SwiftFormatConfiguration
16-
import SwiftFormatCore
1717
import SwiftSyntax
1818
import TSCBasic
1919

20-
/// Runs the linting pipeline over the provided source file.
21-
///
22-
/// If there were any lint diagnostics emitted, this function returns a non-zero exit code.
23-
/// - Parameters:
24-
/// - configuration: The `Configuration` that contains user-specific settings.
25-
/// - sourceFile: A file handle from which to read the source code to be linted.
26-
/// - assumingFilename: The filename of the source file, used in diagnostic output.
27-
/// - ignoreUnparsableFiles: Whether or not to ignore files that contain syntax errors.
28-
/// - debugOptions: The set containing any debug options that were supplied on the command line.
29-
/// - diagnosticEngine: A diagnostic collector that handles diagnostic messages.
30-
/// - Returns: Zero if there were no lint errors, otherwise a non-zero number.
31-
func lintMain(
32-
configuration: Configuration, sourceFile: FileHandle, assumingFilename: String?,
33-
ignoreUnparsableFiles: Bool, debugOptions: DebugOptions, diagnosticEngine: DiagnosticEngine
34-
) {
35-
let linter = SwiftLinter(configuration: configuration, diagnosticEngine: diagnosticEngine)
36-
linter.debugOptions = debugOptions
37-
let assumingFileURL = URL(fileURLWithPath: assumingFilename ?? "<stdin>")
20+
extension SwiftFormatCommand {
21+
/// Formats one or more files containing Swift code.
22+
struct Format: ParsableCommand {
23+
static var configuration = CommandConfiguration(
24+
abstract: "Format Swift source code",
25+
discussion: "When no files are specified, it expects the source from standard input.")
3826

39-
guard let source = readSource(from: sourceFile) else {
40-
diagnosticEngine.diagnose(
41-
Diagnostic.Message(.error, "Unable to read source for linting from \(assumingFileURL.path)."))
42-
return
43-
}
27+
/// Whether or not to format the Swift file in-place.
28+
///
29+
/// If specified, the current file is overwritten when formatting.
30+
@Flag(
31+
name: .shortAndLong,
32+
help: "Overwrite the current file when formatting.")
33+
var inPlace: Bool
4434

45-
do {
46-
try linter.lint(source: source, assumingFileURL: assumingFileURL)
47-
} catch SwiftFormatError.fileNotReadable {
48-
let path = assumingFileURL.path
49-
diagnosticEngine.diagnose(
50-
Diagnostic.Message(.error, "Unable to lint \(path): file is not readable or does not exist."))
51-
return
52-
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
53-
guard !ignoreUnparsableFiles else {
54-
return
35+
@OptionGroup()
36+
var formatOptions: LintFormatOptions
37+
38+
func validate() throws {
39+
if inPlace && formatOptions.paths.isEmpty {
40+
throw ValidationError("'--in-place' is only valid when formatting files")
41+
}
42+
}
43+
44+
func run() throws {
45+
let diagnosticEngine = makeDiagnosticEngine()
46+
47+
if formatOptions.paths.isEmpty {
48+
let configuration = try loadConfiguration(
49+
forSwiftFile: nil, configFilePath: formatOptions.configurationPath)
50+
formatMain(
51+
configuration: configuration, sourceFile: FileHandle.standardInput,
52+
assumingFilename: formatOptions.assumeFilename, inPlace: false,
53+
ignoreUnparsableFiles: formatOptions.ignoreUnparsableFiles,
54+
debugOptions: formatOptions.debugOptions, diagnosticEngine: diagnosticEngine)
55+
} else {
56+
try processSources(
57+
from: formatOptions.paths, configurationPath: formatOptions.configurationPath,
58+
diagnosticEngine: diagnosticEngine
59+
) { sourceFile, path, configuration in
60+
formatMain(
61+
configuration: configuration, sourceFile: sourceFile, assumingFilename: path,
62+
inPlace: inPlace, ignoreUnparsableFiles: formatOptions.ignoreUnparsableFiles,
63+
debugOptions: formatOptions.debugOptions, diagnosticEngine: diagnosticEngine)
64+
}
65+
}
66+
67+
try failIfDiagnosticsEmitted(diagnosticEngine)
5568
}
56-
let path = assumingFileURL.path
57-
let location = SourceLocationConverter(file: path, source: source).location(for: position)
58-
diagnosticEngine.diagnose(
59-
Diagnostic.Message(.error, "file contains invalid or unrecognized Swift syntax."),
60-
location: location)
61-
return
62-
} catch {
63-
let path = assumingFileURL.path
64-
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to lint \(path): \(error)"))
65-
return
6669
}
6770
}
6871

@@ -77,7 +80,7 @@ func lintMain(
7780
/// - debugOptions: The set containing any debug options that were supplied on the command line.
7881
/// - diagnosticEngine: A diagnostic collector that handles diagnostic messages.
7982
/// - Returns: Zero if there were no format errors, otherwise a non-zero number.
80-
func formatMain(
83+
private func formatMain(
8184
configuration: Configuration, sourceFile: FileHandle, assumingFilename: String?, inPlace: Bool,
8285
ignoreUnparsableFiles: Bool, debugOptions: DebugOptions, diagnosticEngine: DiagnosticEngine
8386
) {
@@ -137,10 +140,3 @@ func formatMain(
137140
return
138141
}
139142
}
140-
141-
/// Reads from the given file handle until EOF is reached, then returns the contents as a UTF8
142-
/// encoded string.
143-
fileprivate func readSource(from fileHandle: FileHandle) -> String? {
144-
let sourceData = fileHandle.readDataToEndOfFile()
145-
return String(data: sourceData, encoding: .utf8)
146-
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import Foundation
15+
import SwiftFormat
16+
import SwiftFormatConfiguration
17+
import SwiftFormatCore
18+
import SwiftSyntax
19+
import TSCBasic
20+
21+
extension SwiftFormatCommand {
22+
/// Keep the legacy `-m/--mode` flag working temporarily when no other subcommand is specified.
23+
struct LegacyMain: ParsableCommand {
24+
static var configuration = CommandConfiguration(shouldDisplay: false)
25+
26+
enum ToolMode: String, CaseIterable, ExpressibleByArgument {
27+
case format
28+
case lint
29+
case dumpConfiguration = "dump-configuration"
30+
}
31+
32+
/// The mode in which to run the tool.
33+
///
34+
/// If not specified, the tool will be run in format mode.
35+
@Option(
36+
name: .shortAndLong,
37+
default: .format,
38+
help: "The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'.")
39+
var mode: ToolMode
40+
41+
@OptionGroup()
42+
var lintFormatOptions: LintFormatOptions
43+
44+
/// Whether or not to format the Swift file in-place.
45+
///
46+
/// If specified, the current file is overwritten when formatting.
47+
@Flag(
48+
name: .shortAndLong,
49+
help: "Overwrite the current file when formatting ('format' mode only).")
50+
var inPlace: Bool
51+
52+
mutating func validate() throws {
53+
if inPlace && (mode != .format || lintFormatOptions.paths.isEmpty) {
54+
throw ValidationError("'--in-place' is only valid when formatting files")
55+
}
56+
57+
let modeSupportsRecursive = mode == .format || mode == .lint
58+
if lintFormatOptions.recursive && (!modeSupportsRecursive || lintFormatOptions.paths.isEmpty) {
59+
throw ValidationError("'--recursive' is only valid when formatting or linting files")
60+
}
61+
}
62+
63+
func run() throws {
64+
switch mode {
65+
case .format:
66+
var format = Format()
67+
format.inPlace = inPlace
68+
format.formatOptions = lintFormatOptions
69+
try format.run()
70+
71+
case .lint:
72+
var lint = Lint()
73+
lint.lintOptions = lintFormatOptions
74+
try lint.run()
75+
76+
case .dumpConfiguration:
77+
try DumpConfiguration().run()
78+
}
79+
}
80+
}
81+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import Foundation
15+
import SwiftFormat
16+
import SwiftFormatConfiguration
17+
import SwiftSyntax
18+
import TSCBasic
19+
20+
extension SwiftFormatCommand {
21+
/// Emits style diagnostics for one or more files containing Swift code.
22+
struct Lint: ParsableCommand {
23+
static var configuration = CommandConfiguration(
24+
abstract: "Diagnose style issues in Swift source code",
25+
discussion: "When no files are specified, it expects the source from standard input.")
26+
27+
@OptionGroup()
28+
var lintOptions: LintFormatOptions
29+
30+
func run() throws {
31+
let diagnosticEngine = makeDiagnosticEngine()
32+
33+
if lintOptions.paths.isEmpty {
34+
let configuration = try loadConfiguration(
35+
forSwiftFile: nil, configFilePath: lintOptions.configurationPath)
36+
lintMain(
37+
configuration: configuration, sourceFile: FileHandle.standardInput,
38+
assumingFilename: lintOptions.assumeFilename, debugOptions: lintOptions.debugOptions,
39+
diagnosticEngine: diagnosticEngine)
40+
} else {
41+
try processSources(
42+
from: lintOptions.paths, configurationPath: lintOptions.configurationPath,
43+
diagnosticEngine: diagnosticEngine
44+
) { sourceFile, path, configuration in
45+
lintMain(
46+
configuration: configuration, sourceFile: sourceFile, assumingFilename: path,
47+
debugOptions: lintOptions.debugOptions, diagnosticEngine: diagnosticEngine)
48+
}
49+
}
50+
51+
try failIfDiagnosticsEmitted(diagnosticEngine)
52+
}
53+
}
54+
}
55+
56+
/// Runs the linting pipeline over the provided source file.
57+
///
58+
/// If there were any lint diagnostics emitted, this function returns a non-zero exit code.
59+
/// - Parameters:
60+
/// - configuration: The `Configuration` that contains user-specific settings.
61+
/// - sourceFile: A file handle from which to read the source code to be linted.
62+
/// - assumingFilename: The filename of the source file, used in diagnostic output.
63+
/// - debugOptions: The set containing any debug options that were supplied on the command line.
64+
/// - diagnosticEngine: A diagnostic collector that handles diagnostic messages.
65+
/// - Returns: Zero if there were no lint errors, otherwise a non-zero number.
66+
private func lintMain(
67+
configuration: Configuration, sourceFile: FileHandle, assumingFilename: String?,
68+
debugOptions: DebugOptions, diagnosticEngine: DiagnosticEngine
69+
) {
70+
let linter = SwiftLinter(configuration: configuration, diagnosticEngine: diagnosticEngine)
71+
linter.debugOptions = debugOptions
72+
let assumingFileURL = URL(fileURLWithPath: assumingFilename ?? "<stdin>")
73+
74+
guard let source = readSource(from: sourceFile) else {
75+
diagnosticEngine.diagnose(
76+
Diagnostic.Message(.error, "Unable to read source for linting from \(assumingFileURL.path)."))
77+
return
78+
}
79+
80+
do {
81+
try linter.lint(source: source, assumingFileURL: assumingFileURL)
82+
} catch SwiftFormatError.fileNotReadable {
83+
let path = assumingFileURL.path
84+
diagnosticEngine.diagnose(
85+
Diagnostic.Message(.error, "Unable to lint \(path): file is not readable or does not exist."))
86+
return
87+
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
88+
let path = assumingFileURL.path
89+
let location = SourceLocationConverter(file: path, source: source).location(for: position)
90+
diagnosticEngine.diagnose(
91+
Diagnostic.Message(.error, "file contains invalid or unrecognized Swift syntax."),
92+
location: location)
93+
return
94+
} catch {
95+
let path = assumingFileURL.path
96+
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to lint \(path): \(error)"))
97+
return
98+
}
99+
}

0 commit comments

Comments
 (0)