// // FormatterTests.swift // SwiftFormat // // Created by Nick Lockwood on 30/08/2016. // Copyright 2016 Nick Lockwood // // Distributed under the permissive MIT license // Get the latest version from here: // // https://github.com/nicklockwood/SwiftFormat // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import SwiftFormat import XCTest final class FormatterTests: XCTestCase { func testRemoveCurrentTokenWhileEnumerating() { let input: [Token] = [ .identifier("foo"), .identifier("bar"), .identifier("baz"), ] var output: [Token] = [] let formatter = Formatter(input, options: .default) formatter.forEachToken { i, token in output.append(token) if i == 1 { formatter.removeToken(at: i) } } XCTAssertEqual(output, input) } func testRemovePreviousTokenWhileEnumerating() { let input: [Token] = [ .identifier("foo"), .identifier("bar"), .identifier("baz"), ] var output: [Token] = [] let formatter = Formatter(input, options: .default) formatter.forEachToken { i, token in output.append(token) if i == 1 { formatter.removeToken(at: i - 1) } } XCTAssertEqual(output, input) } func testRemoveNextTokenWhileEnumerating() { let input: [Token] = [ .identifier("foo"), .identifier("bar"), .identifier("baz"), ] var output: [Token] = [] let formatter = Formatter(input, options: .default) formatter.forEachToken { i, token in output.append(token) if i == 1 { formatter.removeToken(at: i + 1) } } XCTAssertEqual(output, [Token](input.dropLast())) } func testIndexBeforeComment() { let input: [Token] = [ .identifier("foo"), .startOfScope("//"), .space(" "), .commentBody("bar"), .linebreak("\n", 1), ] let formatter = Formatter(input, options: .default) let index = formatter.index(before: 4, where: { !$0.isSpaceOrComment }) XCTAssertEqual(index, 0) } func testIndexBeforeMultilineComment() { let input: [Token] = [ .identifier("foo"), .startOfScope("/*"), .space(" "), .commentBody("bar"), .space(" "), .endOfScope("*/"), .linebreak("\n", 1), ] let formatter = Formatter(input, options: .default) let index = formatter.index(before: 6, where: { !$0.isSpaceOrComment }) XCTAssertEqual(index, 0) } // MARK: enable/disable directives func testDisableRule() { let input = "//swiftformat:disable spaceAroundOperators\nlet foo : Int=5;" let output = "// swiftformat:disable spaceAroundOperators\nlet foo : Int=5\n" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDirectiveInMiddleOfComment() { let input = "//fixme: swiftformat:disable spaceAroundOperators - bug\nlet foo : Int=5;" let output = "// FIXME: swiftformat:disable spaceAroundOperators - bug\nlet foo : Int=5\n" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableAndReEnableRules() { let input = """ // swiftformat:disable indent blankLinesBetweenScopes redundantSelf class Foo { let _foo = "foo" func foo() { print(self._foo) } } // swiftformat:enable indent redundantSelf class Bar { let _bar = "bar" func bar() { print(_bar) } } """ let output = """ // swiftformat:disable indent blankLinesBetweenScopes redundantSelf class Foo { let _foo = "foo" func foo() { print(self._foo) } } // swiftformat:enable indent redundantSelf class Bar { let _bar = "bar" func bar() { print(_bar) } } """ XCTAssertEqual(try format(input + "\n", rules: FormatRules.default).output, output + "\n") } func testDisableAllRules() { let input = "//swiftformat:disable all\nlet foo : Int=5;" XCTAssertEqual(try format(input, rules: FormatRules.default).output, input) } func testDisableAndReEnableAllRules() { let input = """ // swiftformat:disable all class Foo { let _foo = "foo" func foo() { print(self._foo) } } // swiftformat:enable all class Bar { let _bar = "bar" func bar() { print(_bar) } } """ let output = """ // swiftformat:disable all class Foo { let _foo = "foo" func foo() { print(self._foo) } } // swiftformat:enable all class Bar { let _bar = "bar" func bar() { print(_bar) } } """ XCTAssertEqual(try format(input + "\n", rules: FormatRules.default).output, output + "\n") } func testDisableAllRulesAndReEnableOneRule() { let input = "//swiftformat:disable all\nlet foo : Int=5;\n//swiftformat:enable linebreakAtEndOfFile" let output = "//swiftformat:disable all\nlet foo : Int=5;\n//swiftformat:enable linebreakAtEndOfFile\n" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableNext() { let input = "//swiftformat:disable:next all\nlet foo : Int=5;\nlet foo : Int=5;" let output = "// swiftformat:disable:next all\nlet foo : Int=5;\nlet foo: Int = 5\n" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testEnableNext() { let input = "//swiftformat:disable all\n//swiftformat:enable:next all\nlet foo : Int=5;\nlet foo : Int=5;" let output = "//swiftformat:disable all\n//swiftformat:enable:next all\nlet foo: Int = 5\nlet foo : Int=5;" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableThis() { let input = "let foo : Int=5; // swiftformat:disable:this all\nlet foo : Int=5;" let output = "let foo : Int=5; // swiftformat:disable:this all\nlet foo: Int = 5\n" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testEnableThis() { let input = "//swiftformat:disable all\nlet foo : Int=5; //swiftformat:enable:this all\nlet foo : Int=5;" let output = "//swiftformat:disable all\nlet foo: Int = 5 // swiftformat:enable:this all\nlet foo : Int=5;" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableRuleWithMultilineComment() { let input = "/*swiftformat:disable spaceAroundOperators*/let foo : Int=5;" let output = "/* swiftformat:disable spaceAroundOperators */ let foo : Int=5\n" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableAllRulesWithMultilineComment() { let input = "/*swiftformat:disable all*/let foo : Int=5;" let output = "/*swiftformat:disable all*/let foo : Int=5;" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableAndReenableAllRulesWithMultilineComment() { let input = """ /*swiftformat:disable all*/let foo : Int=5;/*swiftformat:enable all*/let foo : Int=5; """ let output = """ /*swiftformat:disable all*/let foo : Int=5; /* swiftformat:enable all */ let foo: Int = 5 """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableNextWithMultilineComment() { let input = "/*swiftformat:disable:next all*/\nlet foo : Int=5;\nlet foo : Int=5;" let output = "/* swiftformat:disable:next all */\nlet foo : Int=5;\nlet foo: Int = 5\n" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testEnableNextWithMultilineComment() { let input = "//swiftformat:disable all\n/*swiftformat:enable:next all*/\nlet foo : Int=5;\nlet foo : Int=5;" let output = "//swiftformat:disable all\n/*swiftformat:enable:next all*/\nlet foo: Int = 5\nlet foo : Int=5;" XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testDisableLinewrap() { let input = """ // swiftformat:disable all let foo = bar.baz(some: param).quux("a string of some sort") """ let options = FormatOptions(maxWidth: 10) XCTAssertEqual(try format(input, rules: FormatRules.default, options: options).output, input) } func testMalformedDirective() { let input = "// swiftformat:disbible all" XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssertEqual("\(error)", "Unknown directive 'swiftformat:disbible' on line 1") } } func testMalformedDirective2() { let input = "// swiftformat: --disable all" XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssertEqual("\(error)", "Expected directive after 'swiftformat:' prefix on line 1") } } // MARK: options directive func testAllmanOption() { let input = """ // swiftformat:options --allman true func foo() { print("bar") } """ let output = """ // swiftformat:options --allman true func foo() { print("bar") } """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testAllmanThis() { let input = """ func foo() // swiftformat:options:this --allman true { print("bar") } func foo() { // swiftformat:options:this --allman true print("bar") } """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, input) } func testAllmanNext() { let input = """ func foo() // swiftformat:options:next --allman true { print("bar") } """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, input) } func testAllmanPrevious() { let input = """ func foo() { // swiftformat:options:previous --allman true print("bar") } """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, input) } func testIndentNext() { let input = """ class Foo { // swiftformat:options:next --indent 2 func bar() { print("bar") } func baz() { print("bar") } } """ let output = """ class Foo { // swiftformat:options:next --indent 2 func bar() { print("bar") } func baz() { print("bar") } } """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testSwiftVersionNext() { let input = """ // swiftformat:options:next --swiftversion 5.2 let foo1 = bar.map { $0.foo } let foo2 = bar.map { $0.foo } """ let output = """ // swiftformat:options:next --swiftversion 5.2 let foo1 = bar.map(\\.foo) let foo2 = bar.map { $0.foo } """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testCumulativeOptions() { let input = """ // swiftformat:options --self insert // swiftformat:options:next --swiftversion 5.2 let foo1 = self.map { $0.foo } // swiftformat:options --self remove let foo2 = self.map { $0.foo } """ let output = """ // swiftformat:options --self insert // swiftformat:options:next --swiftversion 5.2 let foo1 = self.map(\\.foo) // swiftformat:options --self remove let foo2 = map { $0.foo } """ XCTAssertEqual(try format(input, rules: FormatRules.default).output, output) } func testMalformedOption() { let input = """ // swiftformat:options blooblahbleh """ XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssert("\(error)".contains("Unknown option blooblahbleh")) } } func testInvalidOption() { let input = """ // swiftformat:options --foobar baz """ XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssert("\(error)".contains("Unknown option --foobar")) } } func testInvalidOptionValue() { let input = """ // swiftformat:options --indent baz """ XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssertEqual("\(error)", "Unsupported --indent value 'baz' on line 1") } } func testInvalidEnumOptionValue() { let input = """ // swiftformat:options --else-position prev-line """ XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssertEqual("\(error)", """ Unsupported --else-position value 'prev-line' on line 1. Valid options are "same-line" or "next-line" """) } } func testInvalidEnumOptionValue2() { let input = """ // swiftformat:options --else-position next """ XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssertEqual("\(error)", """ Unsupported --else-position value 'next' on line 1. Did you mean 'next-line'? """) } } func testInvalidBoolOptionValue() { let input = """ // swiftformat:options --allman always """ XCTAssertThrowsError(try format(input, rules: FormatRules.default).output) { error in XCTAssertEqual("\(error)", """ Unsupported --allman value 'always' on line 1. Valid options are "true" or "false" """) } } func testDeprecatedOptionValue() { let input = """ // swiftformat:options --ranges spaced """ XCTAssertNoThrow(try format(input, rules: FormatRules.default).output) } // MARK: linebreaks func testLinebreakAfterLinebreakReturnsCorrectIndex() { let formatter = Formatter([ .linebreak("\n", 1), .linebreak("\n", 1), ]) XCTAssertEqual(formatter.linebreakToken(for: 1), .linebreak("\n", 1)) } func testOriginalLinePreservedAfterFormatting() { let formatter = Formatter([ .identifier("foo"), .space(" "), .startOfScope("{"), .linebreak("\n", 1), .linebreak("\n", 2), .space(" "), .identifier("bar"), .linebreak("\n", 3), .endOfScope("}"), ]) FormatRule.blankLinesAtStartOfScope.apply(with: formatter) XCTAssertEqual(formatter.tokens, [ .identifier("foo"), .space(" "), .startOfScope("{"), .linebreak("\n", 2), .space(" "), .identifier("bar"), .linebreak("\n", 3), .endOfScope("}"), ]) } // MARK: Format range func testCodeOutsideRangeNotFormatted() throws { let input = tokenize(""" func foo () { var bar = 5 } """) for range in [0 ..< 2, 5 ..< 7, 14 ..< 16, 17 ..< 19] { XCTAssertEqual(try sourceCode( for: format( input, rules: FormatRules.all, range: range ).tokens ), sourceCode(for: input), "range \(range)") } let output1 = tokenize(""" func foo () { var bar = 5 } """) XCTAssertEqual(try format( input, rules: [.consecutiveSpaces], range: 10 ..< 13 ).tokens, output1) let output2 = """ func foo () { var bar = 5 } """ XCTAssertEqual(try sourceCode(for: format( input, rules: [.blankLinesAtStartOfScope], range: 6 ..< 9 ).tokens), output2) } // MARK: format line range func testFormattingRange() { let input = """ let badlySpaced1:Int = 5 let badlySpaced2:Int=5 let badlySpaced3 : Int = 5 """ let output = """ let badlySpaced1:Int = 5 let badlySpaced2: Int = 5 let badlySpaced3 : Int = 5 """ XCTAssertEqual(try format(input, lineRange: 2 ... 2).output, output) } func testFormattingRange2() { let input = """ enum ImagesToShow { case none case mentioned case all } """ let output = """ enum ImagesToShow { case none case mentioned case all } """ let options = FormatOptions(allmanBraces: true) XCTAssertEqual(try format(input, options: options, lineRange: 1 ... 2).output, output) } func testFormattingRangeNoCrash() { let input = """ func foo() { if bar { print( "foo") } } """ let output = """ func foo() { if bar { print("foo") } } """ let inputTokens = tokenize(input), outputTokens = tokenize(output) XCTAssertEqual(tokenRange(forLineRange: 3 ... 4, in: inputTokens), 14 ..< 26) XCTAssertEqual(tokenRange(forLineRange: 3 ... 4, in: outputTokens), 14 ..< 25) XCTAssertEqual(try format(input, lineRange: 3 ... 4).output, output) } // MARK: endOfScope func testEndOfScopeInSwitch() { let formatter = Formatter(tokenize(""" switch foo { case bar: break } """)) XCTAssertEqual(formatter.endOfScope(at: 4), 13) } // MARK: change tracking func testTrackChangesInFirstLine() { let formatter = Formatter(tokenize("foo bar\nbaz"), trackChanges: true) let tokens = formatter.tokens formatter.removeLastToken() XCTAssertNotEqual(formatter.tokens, tokens) XCTAssertEqual(formatter.changes.count, 1) XCTAssertEqual(formatter.changes.first?.line, 2) } func testTrackChangesInSecondLine() throws { let formatter = Formatter(tokenize("foo\nbar\nbaz"), trackChanges: true) let tokens = formatter.tokens try formatter.removeToken(at: XCTUnwrap(formatter.tokens.firstIndex(of: .identifier("bar")))) XCTAssertNotEqual(formatter.tokens, tokens) XCTAssertEqual(formatter.changes.count, 1) XCTAssertEqual(formatter.changes.first?.line, 2) } func testTrackChangesInLastLine() { let formatter = Formatter(tokenize("foo\nbar\nbaz"), trackChanges: true) let tokens = formatter.tokens formatter.removeLastToken() XCTAssertNotEqual(formatter.tokens, tokens) XCTAssertEqual(formatter.changes.count, 1) XCTAssertEqual(formatter.changes.first?.line, 3) } func testTrackChangesInSingleLine() { let formatter = Formatter(tokenize("foo bar"), trackChanges: true) let tokens = formatter.tokens formatter.removeToken(at: 0) XCTAssertNotEqual(formatter.tokens, tokens) XCTAssertEqual(formatter.changes.count, 1) } func testTrackChangesIgnoresLinebreakIndex() { let formatter = Formatter(tokenize("\n\n"), trackChanges: true) var tokens = formatter.tokens tokens.insert(tokens.removeLast(), at: 0) XCTAssertNotEqual(formatter.tokens, tokens) formatter.replaceTokens(in: 0 ..< 2, with: tokens) XCTAssert(formatter.changes.isEmpty) } func testTrackRemovalOfBlankLineFollowedByBlankLine() { let formatter = Formatter(tokenize("foo\n\n\n"), trackChanges: true) let tokens = formatter.tokens formatter.removeToken(at: 2) XCTAssertNotEqual(formatter.tokens, tokens) XCTAssertEqual(formatter.changes.count, 1) XCTAssertEqual(formatter.changes.first?.line, 2) } func testTrackRemovalOfBlankLineAfterBlankLine() { let formatter = Formatter(tokenize("foo\n\n\n"), trackChanges: true) let tokens = formatter.tokens formatter.removeLastToken() XCTAssertNotEqual(formatter.tokens, tokens) XCTAssertEqual(formatter.changes.count, 1) XCTAssertEqual(formatter.changes.first?.line, 3) } func testMoveTokensToEarlierPositionTrackedAsMoves() { let formatter = Formatter(tokenize("foo()\nbar()\n"), trackChanges: true) formatter.moveTokens(in: 4 ... 7, to: 0) XCTAssertEqual(sourceCode(for: formatter.tokens), "bar()\nfoo()\n") XCTAssert(!formatter.changes.isEmpty) XCTAssert(formatter.changes.allSatisfy(\.isMove)) } func testMoveTokensToFollowingPositionTrackedAsMoves() { let formatter = Formatter(tokenize("foo()\nbar()\n"), trackChanges: true) formatter.moveTokens(in: 0 ... 3, to: 8) XCTAssertEqual(sourceCode(for: formatter.tokens), "bar()\nfoo()\n") XCTAssert(!formatter.changes.isEmpty) XCTAssert(formatter.changes.allSatisfy(\.isMove)) } func testReplaceAllTokensTracksMoves() { let input: [Token] = [ tokenize("foo()"), [.linebreak("\n", 0)], [.linebreak("\n", 1)], tokenize("foobar()"), [.linebreak("\n", 2)], [.linebreak("\n", 3)], tokenize("bar()"), [.linebreak("\n", 4)], [.linebreak("\n", 5)], tokenize("baaz()"), [.linebreak("\n", 6)], ].flatMap { $0 } let output: [Token] = [ tokenize("bar()"), [.linebreak("\n", 4)], [.linebreak("\n", 1)], tokenize("barfoo()"), [.linebreak("\n", 2)], [.linebreak("\n", 3)], tokenize("foo()"), [.linebreak("\n", 0)], [.linebreak("\n", 5)], tokenize("quux()"), [.linebreak("\n", 6)], ].flatMap { $0 } let formatter = Formatter(input, trackChanges: true) formatter.diffAndReplaceTokens(in: ClosedRange(formatter.tokens.indices), with: output) XCTAssertEqual(sourceCode(for: formatter.tokens), sourceCode(for: output)) // The changes should include both moves and non-moves XCTAssert(formatter.changes.contains(where: \.isMove)) XCTAssert(formatter.changes.contains(where: { !$0.isMove })) } }