Skip to content

Commit 72d8ba7

Browse files
authored
Merge pull request swiftlang#229 from dabelknap/recursive
Add option for running recursively
2 parents 1e5a6b1 + a451825 commit 72d8ba7

File tree

7 files changed

+152
-7
lines changed

7 files changed

+152
-7
lines changed

Sources/SwiftFormat/SwiftFormatError.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
public enum SwiftFormatError: Error {
14-
case fileNotFound
14+
case fileNotReadable
15+
case isDirectory
1516
}

Sources/SwiftFormat/SwiftFormatter.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ public final class SwiftFormatter {
5151
contentsOf url: URL, to outputStream: inout Output
5252
) throws {
5353
guard FileManager.default.isReadableFile(atPath: url.path) else {
54-
throw SwiftFormatError.fileNotFound
54+
throw SwiftFormatError.fileNotReadable
55+
}
56+
var isDir: ObjCBool = false
57+
if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue {
58+
throw SwiftFormatError.isDirectory
5559
}
5660
let sourceFile = try SyntaxTreeParser.parse(url)
5761
try format(syntax: sourceFile, assumingFileURL: url, to: &outputStream)

Sources/SwiftFormat/SwiftLinter.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ public final class SwiftLinter {
4747
/// - Throws: If an unrecoverable error occurs when formatting the code.
4848
public func lint(contentsOf url: URL) throws {
4949
guard FileManager.default.isReadableFile(atPath: url.path) else {
50-
throw SwiftFormatError.fileNotFound
50+
throw SwiftFormatError.fileNotReadable
51+
}
52+
var isDir: ObjCBool = false
53+
if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue {
54+
throw SwiftFormatError.isDirectory
5155
}
5256
let sourceFile = try SyntaxTreeParser.parse(url)
5357
try lint(syntax: sourceFile, assumingFileURL: url)

Sources/swift-format/CommandLineOptions.swift

+30
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ struct CommandLineOptions {
3333
/// If specified, the current file is overwritten when formatting
3434
var inPlace: Bool = false
3535

36+
/// Whether or not to run the formatter/linter recursively.
37+
///
38+
/// If set, we recursively run on all ".swift" files in any provided directories.
39+
var recursive: Bool = false
40+
3641
/// Advanced options that are useful for developing/debugging but otherwise not meant for general
3742
/// use.
3843
var debugOptions: DebugOptions = []
@@ -100,6 +105,15 @@ func processArguments(commandName: String, _ arguments: [String]) -> CommandLine
100105
)) {
101106
$0.inPlace = $1
102107
}
108+
binder.bind(
109+
option: parser.add(
110+
option: "--recursive",
111+
shortName: "-r",
112+
kind: Bool.self,
113+
usage: "Recursively run on '.swift' files in any provided directories."
114+
)) {
115+
$0.recursive = $1
116+
}
103117

104118
// Add advanced debug/developer options. These intentionally have no usage strings, which omits
105119
// them from the `--help` screen to avoid noise for the general user.
@@ -130,6 +144,22 @@ func processArguments(commandName: String, _ arguments: [String]) -> CommandLine
130144
if opts.inPlace && ToolMode.format != opts.mode {
131145
throw ArgumentParserError.unexpectedArgument("--in-place, -i")
132146
}
147+
148+
if opts.recursive && !(ToolMode.format == opts.mode || ToolMode.lint == opts.mode) {
149+
throw ArgumentParserError.unexpectedArgument("--recursive, -r")
150+
}
151+
152+
if !opts.paths.isEmpty && !opts.recursive {
153+
for path in opts.paths {
154+
var isDir: ObjCBool = false
155+
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir), isDir.boolValue {
156+
throw ArgumentParserError.invalidValue(
157+
argument: "'\(path)'",
158+
error: ArgumentConversionError.custom("for directories, use --recursive option")
159+
)
160+
}
161+
}
162+
}
133163
} catch {
134164
stderrStream.write("error: \(error)\n\n")
135165
parser.printUsage(on: stderrStream)
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2018 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 Foundation
14+
15+
/// Iterator for looping over lists of files and directories. Directories are automatically
16+
/// traversed recursively, and we check for files with a ".swift" extension.
17+
struct FileIterator: Sequence, IteratorProtocol {
18+
19+
/// List of file and directory paths to iterate over.
20+
let paths: [String]
21+
22+
/// Iterator for "paths" list.
23+
var pathIterator: Array<String>.Iterator
24+
25+
/// Iterator for recursing through directories.
26+
var dirIterator: FileManager.DirectoryEnumerator? = nil
27+
28+
/// Keep track of the current directory we're recursing through.
29+
var currentDirectory: String = ""
30+
31+
/// Keep track of paths we have visited to prevent duplicates.
32+
var visited: Set<String> = []
33+
34+
/// The file extension to check for when recursing through directories.
35+
let fileSuffix = ".swift"
36+
37+
/// The input is a list of paths as Strings. Some will be file paths, and others directories.
38+
public init(paths: [String]) {
39+
self.paths = paths
40+
self.pathIterator = self.paths.makeIterator()
41+
}
42+
43+
/// Iterate through the "paths" list, and emit the file paths in it. If we encounter a directory,
44+
/// recurse through it and emit .swift file paths.
45+
mutating func next() -> String? {
46+
var output: String? = nil
47+
while output == nil {
48+
// Check if we're recursing through a directory
49+
if dirIterator != nil {
50+
output = nextInDirectory()
51+
}
52+
else {
53+
guard let next = pathIterator.next() else { return nil }
54+
var isDir: ObjCBool = false
55+
if FileManager.default.fileExists(atPath: next, isDirectory: &isDir) {
56+
if isDir.boolValue {
57+
dirIterator = FileManager.default.enumerator(atPath: next)
58+
currentDirectory = next
59+
}
60+
else { output = next }
61+
}
62+
else {
63+
// If a path doesn't exist, allow it pass down into the SwiftFormat API so it can throw
64+
// the appropriate exception. We don't want to kill the entire process if this happens.
65+
output = next
66+
}
67+
}
68+
if let out = output, visited.contains(out) { output = nil }
69+
}
70+
if let out = output { visited.insert(out) }
71+
return output
72+
}
73+
74+
/// Use the FileManager API to recurse through directories and emit .swift file paths.
75+
private mutating func nextInDirectory() -> String? {
76+
var output: String? = nil
77+
while output == nil {
78+
var isDir: ObjCBool = false
79+
if let item = dirIterator?.nextObject() as? String {
80+
if item.hasSuffix(fileSuffix)
81+
&& FileManager.default.fileExists(
82+
atPath: currentDirectory + "/" + item, isDirectory: &isDir)
83+
&& !isDir.boolValue
84+
{
85+
output = currentDirectory + "/" + item
86+
}
87+
}
88+
else { break }
89+
}
90+
// If we've exhausted the files in the directory recursion, unset the directory iterator.
91+
if output == nil { dirIterator = nil }
92+
return output
93+
}
94+
}

Sources/swift-format/Run.swift

+14-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ func lintMain(configuration: Configuration, path: String) -> Int {
3030

3131
do {
3232
try linter.lint(contentsOf: url)
33-
} catch {
33+
}
34+
catch SwiftFormatError.fileNotReadable {
35+
stderrStream.write("Unable to lint \(path): file is not readable or does not exist.\n")
36+
stderrStream.flush()
37+
return 1
38+
}
39+
catch {
3440
stderrStream.write("Unable to lint \(path): \(error)\n")
3541
stderrStream.flush()
3642
exit(1)
@@ -68,7 +74,13 @@ func formatMain(
6874
try formatter.format(contentsOf: url, to: &stdoutStream)
6975
stdoutStream.flush()
7076
}
71-
} catch {
77+
}
78+
catch SwiftFormatError.fileNotReadable {
79+
stderrStream.write("Unable to format \(path): file is not readable or does not exist.\n")
80+
stderrStream.flush()
81+
return 1
82+
}
83+
catch {
7284
stderrStream.write("Unable to format \(path): \(error)\n")
7385
stderrStream.flush()
7486
exit(1)

Sources/swift-format/main.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ fileprivate func main(_ arguments: [String]) -> Int32 {
2323
switch options.mode {
2424
case .format:
2525
var ret = 0
26-
for path in options.paths {
26+
for path in FileIterator(paths: options.paths) {
2727
let configuration = loadConfiguration(
2828
forSwiftFile: path, configFilePath: options.configurationPath)
2929
ret |= formatMain(
@@ -35,7 +35,7 @@ fileprivate func main(_ arguments: [String]) -> Int32 {
3535
return Int32(ret)
3636
case .lint:
3737
var ret = 0
38-
for path in options.paths {
38+
for path in FileIterator(paths: options.paths) {
3939
let configuration = loadConfiguration(
4040
forSwiftFile: path, configFilePath: options.configurationPath)
4141
ret |= lintMain(configuration: configuration, path: path)

0 commit comments

Comments
 (0)