Added inferoptions feature

This commit is contained in:
Nick Lockwood
2016-11-16 08:40:49 +00:00
parent b58f4be7c1
commit 6ea4e6485f
5 changed files with 202 additions and 68 deletions
+3 -2
View File
@@ -2,9 +2,10 @@
## [0.18](https://github.com/nicklockwood/SwiftFormat/releases/tag/0.18) (2016-11-??)
- Added `--ifdef` command line option for controlling how `#if`...`#endif` clauses are indented
- Added `--inferoptions` command line argument for auto-configuring format options from existing source
- Added `--ifdef` command line argument for controlling how `#if`...`#endif` clauses are indented
- Added `redundantGet` rule to remove unneeded `get {}` clause in read-only computed properties
- `redundantParens` now removed unneeded parens from expressions and closure arguments
- `redundantParens` now removes unneeded parens from expressions and closure arguments
## [0.17.2](https://github.com/nicklockwood/SwiftFormat/releases/tag/0.17.2) (2016-11-11)
+39 -29
View File
@@ -33,28 +33,6 @@
import Foundation
let arguments = [
"output",
"indent",
"allman",
"linebreaks",
"semicolons",
"commas",
"comments",
"ranges",
"empty",
"trimwhitespace",
"insertlines",
"removelines",
"header",
"ifdef",
"experimental",
"fragment",
"cache",
"help",
"version",
]
func showHelp() {
print("swiftformat, version \(version)")
print("copyright (c) 2016 Nick Lockwood")
@@ -63,6 +41,7 @@ func showHelp() {
print("")
print(" <file> ... input file(s) or directory path(s)")
print(" --output output path for formatted file(s) (defaults to input path)")
print(" --inferoptions path to file or directory from which to infer formatting options")
print(" --indent number of spaces to indent, or \"tab\" to use tabs")
print(" --allman use allman indentation style \"true\" or \"false\" (default)")
print(" --linebreaks linebreak character to use. \"cr\", \"crlf\" or \"lf\" (default)")
@@ -93,7 +72,7 @@ func expandPath(_ path: String) -> URL {
func optionsForArguments(_ args: [String: String]) throws -> FormatOptions {
func processOption(_ key: String, handler: (String) throws -> Void) throws {
precondition(arguments.contains(key))
precondition(commandLineArguments.contains(key))
guard let value = args[key] else {
return
}
@@ -265,8 +244,15 @@ func optionsForArguments(_ args: [String: String]) throws -> FormatOptions {
return options
}
func timeEvent(block: () -> Void) -> String {
let start = CFAbsoluteTimeGetCurrent()
block()
let time = round((CFAbsoluteTimeGetCurrent() - start) * 100) / 100 // round to nearest 10ms
return String(format: "%gs", time)
}
func processArguments(_ args: [String]) {
guard let args = preprocessArguments(args, arguments) else {
guard let args = preprocessArguments(args, commandLineArguments) else {
return
}
@@ -287,6 +273,28 @@ func processArguments(_ args: [String]) {
return
}
// Infer options
if args["inferoptions"] != nil {
if let inferURL = args["inferoptions"].map({ expandPath($0) }) {
print("inferring swiftformat options from source file(s)...")
var files = 0
var arguments = ""
let time = timeEvent {
let (count, options) = inferOptions(from: inferURL)
arguments = commandLineArguments(for: options).map({
"--\($0) \($1)" }).joined(separator: " ")
files = count
}
print("options inferred from \(files) file\(files == 1 ? "" : "s") in \(time)")
print("")
print(arguments)
print("")
} else {
print("error: --inferoptions argument was not a valid path")
}
return
}
// Get input path(s)
var inputURLs = [URL]()
while let inputPath = args[String(inputURLs.count + 1)] {
@@ -391,11 +399,13 @@ func processArguments(_ args: [String]) {
print("running swiftformat...")
// Format the code
let start = CFAbsoluteTimeGetCurrent()
let (filesWritten, filesChecked) =
processInput(inputURLs, andWriteToOutput: outputURL, withOptions: options, cacheURL: cacheURL)
let time = round((CFAbsoluteTimeGetCurrent() - start) * 100) / 100 // round to nearest 10ms
print("swiftformat completed. \(filesWritten)/\(filesChecked) file\(filesChecked == 1 ? "" : "s") updated in \(String(format: "%g", time))s")
var filesWritten = 0, filesChecked = 0
let time = timeEvent {
(filesWritten, filesChecked) =
processInput(inputURLs, andWriteToOutput: outputURL, withOptions: options, cacheURL: cacheURL)
}
print("swiftformat completed. \(filesWritten)/\(filesChecked) " +
"file\(filesChecked == 1 ? "" : "s") updated in \(time)")
}
// Pass in arguments minus program itself
+18 -8
View File
@@ -48,9 +48,13 @@ Command-line tool
**Usage:**
If you followed the installation instructions above, you can now just type `swiftformat .` (that's a space and then a period after the command) in the terminal to format any swift files in the current directory.
If you followed the installation instructions above, you can now just type
**WARNING:** `swiftformat .` will overwrite any swift files it finds in the current directory, and any subfolders therein. If you run it from your home directory, it will probably reformat every swift file on your hard drive.
swiftformat .
(that's a space and then a period after the command) in the terminal to format any Swift files in the current directory.
**WARNING:** `swiftformat .` will overwrite any Swift files it finds in the current directory, and any subfolders therein. If you run it from your home directory, it will probably reformat every Swift file on your hard drive.
To use it safely, do the following:
@@ -58,13 +62,19 @@ To use it safely, do the following:
2. Make sure that you have committed all your changes to that code safely in git (or whatever source control system you use. If you don't use source control, rethink your life choices).
3. In Terminal, type `swiftformat "/path/to/your/code/"` (the path can either be absolute, or relative to the current directory. The `""` quotes around the path are optional, but if the path contains spaces then you either need to use quotes, or escape each space with `\`).
3. (Optional) In Terminal, type `swiftformat --inferoptions "/path/to/your/code/"`. This will suggest a set of formatting options to use that match your existing project style (but you are free to ignore these and use the defaults, or your own settings if you prefer).
4. Use your source control system to check the changes, and verify that no undesirable changes have been introduced (if they have, file a bug).
The path can point to either a single Swift file, or a directory of files. It can be either be absolute, or relative to the current directory. The `""` quotes around the path are optional, but if the path contains spaces then you either need to use quotes, or escape each space with `\`.
5. (Optional) commit the changes.
4. In Terminal, type `swiftformat "/path/to/your/code/"`. The same rules apply as above with respect to path formatting, but you can enter multiple paths if you wish, separated by spaces.
This *should* ensure that you avoid catastrophic data loss, but in the unlikely event that it wipes your hard drive, **please note that I accept no responsibility**.
If you used `--inferoptions` to generate a suggested set of options in step 3, you should copy and paste them into the command, either before or after the path(s) to your source files.
5. Press enter to begin formatting. Once the formatting is complete, use your source control system to check the changes, and verify that no undesirable changes have been introduced. If they have, revert the changes, tweak the options and try again.
6. (Optional) commit the changes.
Following these instructions *should* ensure that you avoid catastrophic data loss, but in the unlikely event that it wipes your hard drive, **please note that I accept no responsibility**.
If you prefer, you can also use unix pipes to include swiftformat as part of a command chain. For example, this is an alternative way to format a file:
@@ -358,9 +368,9 @@ There haven't been many questions yet, but here's what I'd like to think people
*Q. How can I modify the formatting rules?*
> A. Most of the rules are hard-coded right now, with a handful of options exposed in the `FormatOptions` struct. If you look in `Rules.swift` you will find a list of all the rules that are applied by default. You can disable any rules you don't want and build a new version of the command line tool.
> A. Many configuration options are exposed in the command line interface. You can either set these manually, or use the `--inferoptions` argument to automatically generate the configuration from your existing project.
> With a bit more effort, you can also edit the existing rules or create new ones. If you think your changes might be generally useful, make a pull request.
> If the options you want aren't exposed, the rules are implemented as functions in the file `Rules.swift`, so you can modify these and build a new version of the command line tool. If you think your changes might be generally useful, make a pull request.
*Q. Why did you write yet another Swift formatting tool?*
+127 -29
View File
@@ -71,10 +71,58 @@ public func enumerateSwiftFiles(withInputURL inputURL: URL, outputURL: URL? = ni
}
}
/// Parse an input file or directory and write it to the specified output path
/// Returns the number of files that were written
public func processInput(_ inputURLs: [URL], andWriteToOutput outputURL: URL? = nil,
withOptions options: FormatOptions, cacheURL: URL? = nil) -> (Int, Int) {
/// Format a pre-parsed token array
public func format(_ tokens: [Token],
rules: [FormatRule] = defaultRules,
options: FormatOptions = FormatOptions()) throws -> String {
// Parse
guard options.fragment || tokens.last?.isError == false else {
// TODO: more useful errors
throw NSError(domain: "SwiftFormat", code: 0, userInfo: nil)
}
// Format
let formatter = Formatter(tokens, options: options)
rules.forEach { $0(formatter) }
// Output
var output = ""
for token in formatter.tokens { output += token.string }
return output
}
/// Format code with specified rules and options
public func format(_ source: String,
rules: [FormatRule] = defaultRules,
options: FormatOptions = FormatOptions()) throws -> String {
return try format(tokenize(source), rules: rules, options: options)
}
// MARK: Internal APIs used by CLI - included here for testing purposes
func inferOptions(from inputURL: URL) -> (Int, FormatOptions) {
var tokens = [Token]()
var filesChecked = 0
enumerateSwiftFiles(withInputURL: inputURL) { inputURL, _ in
if let input = try? String(contentsOf: inputURL) {
let _tokens = tokenize(input)
if _tokens.last?.isError == false {
filesChecked += 1
tokens += _tokens
} else {
print("error: could not parse file: \(inputURL.path)")
}
} else {
print("error: failed to read file: \(inputURL.path)")
}
}
return (filesChecked, inferOptions(tokens))
}
func processInput(_ inputURLs: [URL], andWriteToOutput outputURL: URL? = nil,
withOptions options: FormatOptions, cacheURL: URL? = nil) -> (Int, Int) {
// Load cache
let cachePrefix = version + String(describing: options)
let cacheDirectory = cacheURL?.deletingLastPathComponent().absoluteURL
@@ -173,31 +221,81 @@ func preprocessArguments(_ args: [String], _ names: [String]) -> [String: String
return namedArgs
}
/// Format a pre-parsed token array
public func format(_ tokens: [Token],
rules: [FormatRule] = defaultRules,
options: FormatOptions = FormatOptions()) throws -> String {
// Parse
guard options.fragment || tokens.last?.isError == false else {
// TODO: more useful errors
throw NSError(domain: "SwiftFormat", code: 0, userInfo: nil)
func commandLineArguments(for options: FormatOptions) -> [String: String] {
var args = [String: String]()
for child in Mirror(reflecting: options).children {
if let label = child.label {
switch label {
case "indent":
if options.indent == "\t" {
args["indent"] = "tabs"
} else {
args["indent"] = String(options.indent.characters.count)
}
case "linebreak":
switch options.linebreak {
case "\r":
args["linebreaks"] = "cr"
case "\n":
args["linebreaks"] = "lf"
case "\r\n":
args["linebreaks"] = "crlf"
default:
break
}
case "allowInlineSemicolons":
args["semicolons"] = options.allowInlineSemicolons ? "inline" : "never"
case "spaceAroundRangeOperators":
args["ranges"] = options.spaceAroundRangeOperators ? "spaced" : "nospace"
case "useVoid":
args["empty"] = options.useVoid ? "void" : "tuples"
case "trailingCommas":
args["commas"] = options.trailingCommas ? "always" : "inline"
case "indentComments":
args["comments"] = options.indentComments ? "indent" : "ignore"
case "truncateBlankLines":
args["trimwhitespace"] = options.truncateBlankLines ? "always" : "nonblank-lines"
case "insertBlankLines":
args["insertlines"] = options.insertBlankLines ? "enabled" : "disabled"
case "removeBlankLines":
args["removelines"] = options.removeBlankLines ? "enabled" : "disabled"
case "allmanBraces":
args["allman"] = options.allmanBraces ? "true" : "false"
case "stripHeader":
args["header"] = options.stripHeader ? "strip" : "ignore"
case "ifdefIndentMode":
args["ifdef"] = options.ifdefIndentMode.rawValue
case "experimentalRules":
args["experimental"] = options.experimentalRules ? "enabled" : nil
case "fragment":
args["fragment"] = options.fragment ? "true" : nil
default:
assertionFailure("Unknown option: \(label)")
}
}
}
// Format
let formatter = Formatter(tokens, options: options)
rules.forEach { $0(formatter) }
// Output
var output = ""
for token in formatter.tokens { output += token.string }
return output
return args
}
/// Format code with specified rules and options
public func format(_ source: String,
rules: [FormatRule] = defaultRules,
options: FormatOptions = FormatOptions()) throws -> String {
return try format(tokenize(source), rules: rules, options: options)
}
let commandLineArguments = [
"output",
"inferoptions",
"indent",
"allman",
"linebreaks",
"semicolons",
"commas",
"comments",
"ranges",
"empty",
"trimwhitespace",
"insertlines",
"removelines",
"header",
"ifdef",
"experimental",
"fragment",
"cache",
"help",
"version",
]
+15
View File
@@ -113,4 +113,19 @@ class SwiftFormatTests: XCTestCase {
"version",
])!, output)
}
// MARK: options to arguments
func testCommandLineArgumentsHaveValidNames() {
let arguments = commandLineArguments(for: FormatOptions())
for key in arguments.keys {
XCTAssertTrue(commandLineArguments.contains(key), "\(key) is not a valid argument name")
}
}
func testCommandLineArgumentsAreCorrect() {
let options = FormatOptions()
let output = ["indent": "4", "linebreaks": "lf", "semicolons": "inline", "ranges": "spaced", "empty": "void", "commas": "always", "comments": "indent", "trimwhitespace": "always", "insertlines": "enabled", "removelines": "enabled", "allman": "false", "header": "ignore", "ifdef": "indent"]
XCTAssertEqual(commandLineArguments(for: options), output)
}
}