diff --git a/CommandLineTool/main.swift b/CommandLineTool/main.swift index 4995e53f..aefda729 100644 --- a/CommandLineTool/main.swift +++ b/CommandLineTool/main.swift @@ -38,6 +38,7 @@ let version = "0.14" let arguments = [ "output", "indent", + "allman", "linebreaks", "semicolons", "commas", @@ -45,6 +46,8 @@ let arguments = [ "ranges", "empty", "trimwhitespace", + "insertlines", + "removelines", "experimental", "fragment", "help", @@ -60,6 +63,7 @@ func showHelp() { print(" input file or directory path") print(" --output output path (defaults to input path)") 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)") print(" --semicolons allow semicolons. \"never\" or \"inline\" (default)") print(" --commas commas in collection literals. \"always\" (default) or \"inline\"") @@ -114,6 +118,16 @@ func optionsForArguments(_ args: [String: String]) throws -> FormatOptions { throw NSError() } } + try processOption("allman") { + switch $0 { + case "true", "enabled": + options.allmanBraces = true + case "false", "disabled": + options.allmanBraces = false + default: + throw NSError() + } + } try processOption("semicolons") { switch $0 { case "inline": diff --git a/SwiftFormat/Options.swift b/SwiftFormat/Options.swift index e9d336ec..0d653dbe 100644 --- a/SwiftFormat/Options.swift +++ b/SwiftFormat/Options.swift @@ -19,6 +19,7 @@ public struct FormatOptions { public var truncateBlankLines: Bool public var insertBlankLines: Bool public var removeBlankLines: Bool + public var allmanBraces: Bool public var experimentalRules: Bool public var fragment: Bool @@ -32,6 +33,7 @@ public struct FormatOptions { truncateBlankLines: Bool = true, insertBlankLines: Bool = true, removeBlankLines: Bool = true, + allmanBraces: Bool = false, experimentalRules: Bool = false, fragment: Bool = false) { @@ -43,9 +45,10 @@ public struct FormatOptions { self.trailingCommas = trailingCommas self.indentComments = indentComments self.truncateBlankLines = truncateBlankLines - self.experimentalRules = experimentalRules self.insertBlankLines = insertBlankLines self.removeBlankLines = removeBlankLines + self.allmanBraces = allmanBraces + self.experimentalRules = experimentalRules self.fragment = fragment } } diff --git a/SwiftFormat/Rules.swift b/SwiftFormat/Rules.swift index b0f9b6a5..51d93558 100644 --- a/SwiftFormat/Rules.swift +++ b/SwiftFormat/Rules.swift @@ -669,7 +669,7 @@ public func blankLinesBetweenScopes(_ formatter: Formatter) { } if let nextTokenIndex = formatter.indexOfNextToken(fromIndex: i, matching: { !$0.isWhitespaceOrLinebreak }), let nextToken = formatter.tokenAtIndex(nextTokenIndex), - nextToken.type != .endOfScope && nextToken.type != .error && nextToken.string != "." { + ![.endOfScope, .error].contains(nextToken.type) && ![".", ",", ":", "else", "catch"].contains(nextToken.string) { if let firstLinebreakIndex = formatter.indexOfNextToken(fromIndex: i, matching: { $0.type == .linebreak }), firstLinebreakIndex < nextTokenIndex { if let secondLinebreakIndex = formatter.indexOfNextToken( @@ -756,9 +756,7 @@ public func indent(_ formatter: Formatter) { "switch", "where", "while", - "do", "as", - "catch", "is", "super", "throw", @@ -871,7 +869,7 @@ public func indent(_ formatter: Formatter) { while let token = formatter.tokenAtIndex(i) { switch token.type { case .identifier: - if ["if", "for", "while", "catch", "switch", "guard" /* TODO: get/set/didSet */ ].contains(token.string) { + if ["if", "else", "for", "while", "do", "catch", "switch", "guard" /* TODO: get/set/didSet */ ].contains(token.string) { // Check that it's actually a keyword and not a member property or enum value return formatter.previousNonWhitespaceOrCommentOrLinebreakToken(fromIndex: i)?.string == "." } @@ -1077,62 +1075,88 @@ public func indent(_ formatter: Formatter) { } } -/// Implement K&R-style braces, where opening brace appears on the same line as -/// the related function or keyword, and the closing brace is on its own line, -/// except for inline closures where opening and closing brace are on same line. -public func knrBraces(_ formatter: Formatter) { +// Implement brace-wrapping rules +public func braces(_ formatter: Formatter) { formatter.forEachToken("{") { i, token in - var index = i - 1 - var linebreakIndex: Int? - while let token = formatter.tokenAtIndex(index) { - switch token.type { - case .linebreak: - linebreakIndex = index - case .whitespace, .commentBody: - break - case .startOfScope: - if token.string != "/*" && token.string != "//" { - fallthrough + if formatter.options.allmanBraces { + // Check this isn't an inline block + guard let nextLinebreakIndex = formatter.indexOfNextToken(fromIndex: i, matching: { + return $0.type == .linebreak + }), let closingBraceIndex = formatter.indexOfNextToken(fromIndex: i, matching: { + return $0.type == .endOfScope && $0.string == "}" + }), nextLinebreakIndex < closingBraceIndex else { return } + // Implement Allman-style braces, where opening brace appears on the next line + if let previousTokenIndex = formatter.indexOfPreviousToken(fromIndex: i, matching: { + $0.type != .whitespace }), let previousToken = formatter.tokenAtIndex(previousTokenIndex) { + switch previousToken.type { + case .identifier, .endOfScope: + formatter.insertToken(Token(.linebreak, formatter.options.linebreak), atIndex: i) + if let indentToken = formatter.indentTokenForLineAtIndex(i) { + formatter.insertToken(indentToken, atIndex: i + 1) + } + if formatter.tokens[i - 1].type == .whitespace { + formatter.removeTokenAtIndex(i - 1) + } + default: + break } - case .endOfScope: - if token.string != "*/" { - fallthrough - } - default: - if let linebreakIndex = linebreakIndex { - formatter.removeTokensInRange(Range(linebreakIndex ... i)) - formatter.insertToken(Token(.whitespace, " "), atIndex: index + 1) - formatter.insertToken(Token(.startOfScope, "{"), atIndex: index + 2) - } - return } - index -= 1 + } else { + // Implement K&R-style braces, where opening brace appears on the same line + var index = i - 1 + var linebreakIndex: Int? + while let token = formatter.tokenAtIndex(index) { + switch token.type { + case .linebreak: + linebreakIndex = index + case .whitespace, .commentBody: + break + case .startOfScope: + if token.string != "/*" && token.string != "//" { + fallthrough + } + case .endOfScope: + if token.string != "*/" { + fallthrough + } + default: + if let linebreakIndex = linebreakIndex { + formatter.removeTokensInRange(Range(linebreakIndex ... i)) + formatter.insertToken(Token(.whitespace, " "), atIndex: index + 1) + formatter.insertToken(Token(.startOfScope, "{"), atIndex: index + 2) + } + return + } + index -= 1 + } } } } /// Ensure that an `else` statement following `if { ... }` appears on the same line /// as the closing brace. This has no effect on the `else` part of a `guard` statement -public func elseOnSameLine(_ formatter: Formatter) { - formatter.forEachToken("else") { i, token in - var index = i - 1 - var containsLinebreak = false - while let token = formatter.tokenAtIndex(index) { - switch token.type { - case .linebreak: - containsLinebreak = true - case .whitespace: - break - case .endOfScope: - if token.string == "}" && containsLinebreak && - formatter.previousNonWhitespaceToken(fromIndex: index)?.type == .linebreak { - formatter.replaceTokensInRange(index + 1 ..< i, with: [Token(.whitespace, " ")]) +public func elseOrCatchOnSameLine(_ formatter: Formatter) { + formatter.forEachToken(ofType: .identifier) { i, token in + if token.string == "else" || token.string == "catch" { + if let prevTokenIndex = formatter.indexOfPreviousToken(fromIndex: i, matching: { + !$0.isWhitespaceOrLinebreak }), let prevToken = formatter.tokenAtIndex(prevTokenIndex), + prevToken.type == .endOfScope, prevToken.string == "}" { + // Only applies to dangling braces + if formatter.previousNonWhitespaceToken(fromIndex: prevTokenIndex)?.type == .linebreak { + if let prevLinebreakIndex = formatter.indexOfPreviousToken(fromIndex: i, matching: { + $0.type == .linebreak }), prevTokenIndex < prevLinebreakIndex { + if !formatter.options.allmanBraces { + formatter.replaceTokensInRange(prevTokenIndex + 1 ..< i, with: [Token(.whitespace, " ")]) + } + } else if formatter.options.allmanBraces { + formatter.replaceTokensInRange(prevTokenIndex + 1 ..< i, + with: [Token(.linebreak, formatter.options.linebreak)]) + if let indentToken = formatter.indentTokenForLineAtIndex(i) { + formatter.insertToken(indentToken, atIndex: prevTokenIndex + 2) + } + } } - return - default: - return } - index -= 1 } } } @@ -1339,8 +1363,8 @@ public let defaultRules: [FormatRule] = [ semicolons, specifiers, void, - knrBraces, - elseOnSameLine, + braces, + elseOrCatchOnSameLine, indent, spaceAroundParens, spaceInsideParens, diff --git a/SwiftFormatTests/RulesTests.swift b/SwiftFormatTests/RulesTests.swift index a92f48fc..7e11844f 100644 --- a/SwiftFormatTests/RulesTests.swift +++ b/SwiftFormatTests/RulesTests.swift @@ -1688,66 +1688,116 @@ class RulesTests: XCTestCase { XCTAssertEqual(try! format(input + "\n", rules: defaultRules, options: options), output + "\n") } - // MARK: knrBraces + // MARK: knr braces func testAllmanBracesAreConverted() { let input = "func foo()\n{\n statement\n}" let output = "func foo() {\n statement\n}" - XCTAssertEqual(try! format(input, rules: [knrBraces]), output) + XCTAssertEqual(try! format(input, rules: [braces]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } - func testBracesAfterComment() { + func testKnRBracesAfterComment() { let input = "func foo() // comment\n{\n statement\n}" let output = "func foo() { // comment\n statement\n}" - XCTAssertEqual(try! format(input, rules: [knrBraces]), output) + XCTAssertEqual(try! format(input, rules: [braces]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } - func testBracesAfterMultilineComment() { + func testKnRBracesAfterMultilineComment() { let input = "func foo() /* comment/ncomment */\n{\n statement\n}" let output = "func foo() { /* comment/ncomment */\n statement\n}" - XCTAssertEqual(try! format(input, rules: [knrBraces]), output) + XCTAssertEqual(try! format(input, rules: [braces]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } - func testExtraSpaceNotAddedBeforeBrace() { + func testKnRExtraSpaceNotAddedBeforeBrace() { let input = "foo({ bar })" let output = "foo({ bar })" - XCTAssertEqual(try! format(input, rules: [knrBraces]), output) + XCTAssertEqual(try! format(input, rules: [braces]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } - // MARK: elseOnSameLine + // MARK: allman braces - func testElseOnSameLine() { + func testKnRBracesAreConverted() { + let input = "func foo() {\n statement\n}" + let output = "func foo()\n{\n statement\n}" + let options = FormatOptions(allmanBraces: true) + XCTAssertEqual(try! format(input, rules: [braces], options: options), output) + XCTAssertEqual(try! format(input + "\n", rules: defaultRules, options: options), output + "\n") + } + + func testAllmanBraceInsideParensNotConverted() { + let input = "foo({\n bar\n})" + let output = "foo({\n bar\n})" + let options = FormatOptions(allmanBraces: true) + XCTAssertEqual(try! format(input, rules: [braces], options: options), output) + XCTAssertEqual(try! format(input + "\n", rules: defaultRules, options: options), output + "\n") + } + + func testAllmanBraceDoClauseIndent() { + let input = "do {\n foo\n}" + let output = "do\n{\n foo\n}" + let options = FormatOptions(allmanBraces: true) + XCTAssertEqual(try! format(input, rules: [braces], options: options), output) + XCTAssertEqual(try! format(input + "\n", rules: defaultRules, options: options), output + "\n") + } + + func testAllmanBraceCatchClauseIndent() { + let input = "do {\n try foo\n}\ncatch {\n}" + let output = "do\n{\n try foo\n}\ncatch\n{\n}" + let options = FormatOptions(allmanBraces: true) + XCTAssertEqual(try! format(input, rules: [braces], options: options), output) + XCTAssertEqual(try! format(input + "\n", rules: defaultRules, options: options), output + "\n") + } + + // MARK: elseOrCatchOnSameLine + + func testelseOrCatchOnSameLine() { let input = "if true {\n 1\n}\nelse { 2 }" let output = "if true {\n 1\n} else { 2 }" - XCTAssertEqual(try! format(input, rules: [elseOnSameLine]), output) + XCTAssertEqual(try! format(input, rules: [elseOrCatchOnSameLine]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } - func testElseOnSameLineOnlyAppliedToDanglingBrace() { + func testelseOrCatchOnSameLineOnlyAppliedToDanglingBrace() { let input = "if true { 1 }\nelse { 2 }" let output = "if true { 1 }\nelse { 2 }" - XCTAssertEqual(try! format(input, rules: [elseOnSameLine]), output) + XCTAssertEqual(try! format(input, rules: [elseOrCatchOnSameLine]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } - func testGuardNotAffectedByElseOnSameLine() { + func testGuardNotAffectedByelseOrCatchOnSameLine() { let input = "guard true\nelse { return }" let output = "guard true\nelse { return }" - XCTAssertEqual(try! format(input, rules: [elseOnSameLine]), output) + XCTAssertEqual(try! format(input, rules: [elseOrCatchOnSameLine]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } - func testElseOnSameLineDoesntEatPreviousStatement() { + func testelseOrCatchOnSameLineDoesntEatPreviousStatement() { let input = "if true {}\nguard true else { return }" let output = "if true {}\nguard true else { return }" - XCTAssertEqual(try! format(input, rules: [elseOnSameLine]), output) + XCTAssertEqual(try! format(input, rules: [elseOrCatchOnSameLine]), output) XCTAssertEqual(try! format(input + "\n", rules: defaultRules), output + "\n") } + func testElseNotOnSameLineForAllman() { + let input = "if true\n{\n 1\n} else { 2 }" + let output = "if true\n{\n 1\n}\nelse { 2 }" + let options = FormatOptions(allmanBraces: true) + XCTAssertEqual(try! format(input, rules: [elseOrCatchOnSameLine], options: options), output) + XCTAssertEqual(try! format(input + "\n", rules: defaultRules, options: options), output + "\n") + } + + func testGuardNotAffectedByelseOrCatchOnSameLineForAllman() { + let input = "guard true else { return }" + let output = "guard true else { return }" + let options = FormatOptions(allmanBraces: true) + XCTAssertEqual(try! format(input, rules: [elseOrCatchOnSameLine], options: options), output) + XCTAssertEqual(try! format(input + "\n", rules: defaultRules, options: options), output + "\n") + } + // MARK: trailingCommas func testCommaAddedToSingleItem() {