diff --git a/CommandLineTool/main.swift b/CommandLineTool/main.swift index aa301fe3..c91baa64 100644 --- a/CommandLineTool/main.swift +++ b/CommandLineTool/main.swift @@ -2,7 +2,7 @@ // SwiftFormat // main.swift // -// Version 0.3 +// Version 0.4 // // Created by Nick Lockwood on 12/08/2016. // Copyright 2016 Charcoal Design @@ -33,7 +33,7 @@ import Foundation -let version = "0.3" +let version = "0.4" func processInput(inputURL: NSURL, andWriteToOutput outputURL: NSURL, withOptions options: FormattingOptions) -> Int { let manager = NSFileManager.defaultManager() @@ -144,22 +144,23 @@ func processArguments(args: [String]) { guard let args = preprocessArguments(args, [ "output", "indent", + "semicolons", "help", "version", ]) else { return } - + // Version if args["version"] != nil { print("swiftformat, version \(version)") return } - + // Get input / output paths - let inputPath = args["1"] - let outputPath = args["output"] ?? inputPath - + var inputPath = args["1"] + var outputPath = args["output"] ?? inputPath + // Show help if requested specifically or if no arguments are passed if args["help"] != nil || inputPath == nil { print("swiftformat, version \(version)") @@ -167,22 +168,27 @@ func processArguments(args: [String]) { print("") print("usage: swiftformat [-o path] [-i spaces]") print("") - print(" input file or directory path") - print(" -o, --output output path (defaults to input path)") - print(" -i, --indent number of spaces to indent, or \"tab\" to use tabs") - print(" -h, --help this help page") - print(" -v, --version version information") + print(" input file or directory path") + print(" -o, --output output path (defaults to input path)") + print(" -i, --indent number of spaces to indent, or \"tab\" to use tabs") + print(" -s, --semicolons allow semicolons. values are \"never\" or \"inline\" (default)") + print(" -h, --help this help page") + print(" -v, --version version information") print("") return } - + print("running swiftformat...") - + + // Expand input / output paths + inputPath = NSString(string: inputPath!).stringByExpandingTildeInPath + outputPath = NSString(string: outputPath!).stringByExpandingTildeInPath + // Convert paths to file URLs relative to current directory let directoryURL = NSURL(fileURLWithPath: NSFileManager.defaultManager().currentDirectoryPath) let inputURL = NSURL(fileURLWithPath: inputPath!, relativeToURL: directoryURL) let outputURL = NSURL(fileURLWithPath: outputPath!, relativeToURL: directoryURL) - + // Get options var options = FormattingOptions() if let indent = args["indent"] { @@ -195,7 +201,17 @@ func processArguments(args: [String]) { return } } - + if let semicolons = args["semicolons"] { + if semicolons == "inline" { + options.allowInlineSemicolons = true + } else if semicolons == "never" { + options.allowInlineSemicolons = false + } else { + print("error: unsupported semicolons value: \(semicolons).") + return + } + } + // Format the code let filesWritten = processInput(inputURL, andWriteToOutput: outputURL, withOptions: options) print("swiftformat completed. \(filesWritten) file(s) updated.") diff --git a/CommandLineTool/swiftformat b/CommandLineTool/swiftformat index 7769e307..f4e0ad97 100755 Binary files a/CommandLineTool/swiftformat and b/CommandLineTool/swiftformat differ diff --git a/LICENCE.md b/LICENCE.md index 48817da7..cb274954 100755 --- a/LICENCE.md +++ b/LICENCE.md @@ -1,6 +1,6 @@ SwiftFormat -Version 0.3, August 23rd, 2016 +Version 0.4, August 24th, 2016 Copyright (c) 2016 Nick Lockwood diff --git a/README.md b/README.md index 51a4e2c3..1cf27acb 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,16 @@ Here are all the rules that SwiftFormat currently applies: // MARK - UIScrollViewDelegate --> // MARK: - UIScrollViewDelegate +*semicolons* - removes semicolons at the end of lines and (optionally) replaces inline semicolons with a linebreak: + + let foo = 5; --> let foo = 5 + + let foo = 5; let bar = 6 --> let foo = 5 + let bar = 6 + + return; --> return; + goto(fail) goto(fail) + FAQ ----- @@ -296,16 +306,25 @@ Or begin each line with a `*` (or any other non-whitespace character) What's next? -------------- -I expect people will discover (and hopefully report) a lot of bugs in this first release, so the next step will be to fix all of those. - -There are a bunch of additional rules I'd like to add, such as removing trailing semicolons, or correctly formatting headerdoc comments. +There are a bunch of additional rules I'd like to add, such as correctly formatting headerdoc comments. At some point I should probably add an intermediate parsing stage that identifies high-level constructs such as classes and functions and assembles them into a syntax tree. I did't bother doing this originally because I thought it would be easier to implement formatting at the token level, but in fact this just meant that the logic for distinguishing between syntax constructs had to be split between the tokenizer and the formatting rules, making both of them more complex than they ought to be. + +With a syntax tree in place, it should become possible to add much more sophisticated rules, such as converting uppercase enums to lowercase for Swift 3, etc. Release notes ---------------- +Version 0.4 + +- Added new `semicolons` rule, which removes semicolons wherever it's safe to do so +- Added `--semicolons` command-line argument for enabling inline semicolon stripping +- The `todos` rule now corrects `MARK :` to `MARK:` instead of `MARK: :` +- Paths containing ~ are now handled correctly by the command line tool +- Fixed some bugs in generics and custom operator parsing, and added more tests +- Removed trailing whitespace on blank lines caused by the `indent` rule + Version 0.3 - Fixed several cases where generics were misidentified as operators diff --git a/SwiftFormat/Formatter.swift b/SwiftFormat/Formatter.swift index 160ede7b..e9a69b45 100644 --- a/SwiftFormat/Formatter.swift +++ b/SwiftFormat/Formatter.swift @@ -2,7 +2,7 @@ // SwiftFormat // Formatter.swift // -// Version 0.3 +// Version 0.4 // // Created by Nick Lockwood on 12/08/2016. // Copyright 2016 Charcoal Design @@ -36,7 +36,15 @@ import Foundation /// Configuration options for formatting. These aren't actually used by the /// Formatter class itself, but it makes them available to the format rules. public struct FormattingOptions { - var indent: String = " " + public var indent: String + public var allowInlineSemicolons: Bool + + public init(indent: String = " ", + allowInlineSemicolons: Bool = true) { + + self.indent = indent + self.allowInlineSemicolons = allowInlineSemicolons + } } /// This is a utility class used for manipulating a tokenized source file. @@ -48,20 +56,20 @@ public struct FormattingOptions { public class Formatter { private(set) var tokens: [Token] let options: FormattingOptions - + private var indexStack: [Int] = [] - + init(_ tokens: [Token], options: FormattingOptions) { self.tokens = tokens self.options = options } - + /// Returns the token at the specified index, or nil if index is invalid public func tokenAtIndex(index: Int) -> Token? { guard index >= 0 && index < tokens.count else { return nil } return tokens[index] } - + /// Replaces the token at the specified index with one or more new tokens public func replaceTokenAtIndex(index: Int, with tokens: Token...) { if tokens.count == 0 { @@ -73,7 +81,7 @@ public class Formatter { } } } - + /// Replaces the tokens in the specified range with new tokens public func replaceTokensInRange(range: Range, with tokens: Token...) { let max = min(range.count, tokens.count) @@ -90,7 +98,7 @@ public class Formatter { } } } - + /// Removes the token at the specified indez public func removeTokenAtIndex(index: Int) { tokens.removeAtIndex(index) @@ -100,17 +108,17 @@ public class Formatter { } } } - + /// Removes the tokens in the specified range public func removeTokensInRange(range: Range) { replaceTokensInRange(range) } - + /// Removes the last token public func removeLastToken() { tokens.removeLast() } - + /// Inserts a tokens at the specified index public func insertToken(token: Token, atIndex index: Int) { tokens.insert(token, atIndex: index) @@ -120,7 +128,7 @@ public class Formatter { } } } - + /// Loops through each token in the array. It is safe to mutate the token /// array inside the body block, but note that the index and token arguments /// may not reflect the current token any more after a mutation @@ -134,19 +142,19 @@ public class Formatter { } indexStack.popLast() } - + /// As above, but only loops through tokens with the specified type public func forEachToken(ofType type: TokenType, _ body: (Int, Token) -> Void) { forEachToken(matching: { $0.type == type }, body) } - + /// As above, but only loops through tokens with the specified type and string public func forEachToken(string: String, ofType type: TokenType, _ body: (Int, Token) -> Void) { forEachToken(matching: { return $0.type == type && $0.string == string }, body) } - + /// As above, but only loops through tokens with the specified string. /// Tokens of type `StringBody` and `CommentBody` are ignored, as these /// can't be usefully identified by their string value @@ -155,7 +163,7 @@ public class Formatter { return $0.string == string && $0.type != .StringBody && $0.type != .CommentBody }, body) } - + private func forEachToken(matching condition: (Token) -> Bool, _ body: (Int, Token) -> Void) { forEachToken { index, token in if condition(token) { @@ -200,7 +208,7 @@ public func spaceAroundParens(formatter: Formatter) { return false } } - + formatter.forEachToken("(") { i, token in guard let previousToken = formatter.tokenAtIndex(i - 1) else { return @@ -268,7 +276,7 @@ public func spaceAroundBrackets(formatter: Formatter) { return false } } - + formatter.forEachToken("[") { i, token in guard let previousToken = formatter.tokenAtIndex(i - 1) else { return @@ -396,7 +404,7 @@ public func spaceAroundOperators(formatter: Formatter) { return false } } - + func isRvalue(token: Token) -> Bool { switch token.type { case .Identifier, .Number, .StartOfScope: @@ -405,7 +413,7 @@ public func spaceAroundOperators(formatter: Formatter) { return false } } - + func spaceAfter(identifier: String) -> Bool { switch identifier { case "case", @@ -423,7 +431,7 @@ public func spaceAroundOperators(formatter: Formatter) { return false } } - + var scopeStack: [Token] = [] formatter.forEachToken { i, token in switch token.type { @@ -595,7 +603,7 @@ public func indent(formatter: Formatter) { } return index } - + func nextNonWhitespaceToken(fromIndex index: Int) -> Token? { var index = index while let token = formatter.tokenAtIndex(index) { @@ -606,7 +614,7 @@ public func indent(formatter: Formatter) { } return nil } - + func setIndent(indent: String, atIndex index: Int) { if formatter.tokenAtIndex(index)?.type == .Whitespace { if indent != "" { @@ -618,7 +626,7 @@ public func indent(formatter: Formatter) { formatter.insertToken(Token(.Whitespace, indent), atIndex: index) } } - + var scopeIndexStack: [Int] = [] var scopeStartLineIndexes: [Int] = [] var lastNonWhitespaceOrLinebreakIndex = -1 @@ -626,14 +634,14 @@ public func indent(formatter: Formatter) { var indentStack = [""] var lineIndex = 0 var linewrapped = false - + func currentScope() -> Token? { if let scopeIndex = scopeIndexStack.last { return formatter.tokens[scopeIndex] } return nil } - + func tokenIsEndOfStatement(i: Int) -> Bool { if let token = formatter.tokenAtIndex(i) { switch token.type { @@ -703,7 +711,7 @@ public func indent(formatter: Formatter) { } return true } - + func tokenIsStartOfStatement(i: Int) -> Bool { if let token = formatter.tokenAtIndex(i) { switch token.type { @@ -744,7 +752,7 @@ public func indent(formatter: Formatter) { } return true } - + formatter.forEachToken { i, token in if token.type == .StartOfScope { // Handle start of scope @@ -831,8 +839,12 @@ public func indent(formatter: Formatter) { indentStack.append(indentStack.last ?? "") } lineIndex += 1 - let indent = (indentStack.last ?? "") + (linewrapped ? formatter.options.indent : "") - setIndent(indent, atIndex: i + 1) + setIndent("", atIndex: i + 1) + // Only indent if line isn't blank + if let nextToken = formatter.tokenAtIndex(i + 1) where nextToken.type != .Linebreak { + let indent = (indentStack.last ?? "") + (linewrapped ? formatter.options.indent : "") + setIndent(indent, atIndex: i + 1) + } } } // Track token for line wraps @@ -953,7 +965,161 @@ public func todos(formatter: Formatter) { } } +/// Remove semicolons, except where doing so would change the meaning of the code +public func semicolons(formatter: Formatter) { + func firstNonWhitespaceOrComment(fromIndex index: Int) -> Token? { + var i = index + var scopeStack: [Token] = [] + while let token = formatter.tokenAtIndex(i) { + if let scope = scopeStack.last { + if token.closesScopeForToken(scope) { + scopeStack.popLast() + if token.type == .Linebreak { + return token + } + } + } else { + switch token.type { + case .Whitespace: + break + case .StartOfScope: + if token.string == "/*" || token.string == "//" { + scopeStack.append(token) + } else { + return token + } + default: + return token + } + } + i += 1 + } + return nil + } + + func firstNonWhitespaceOrCommentOrLinebreak(fromIndex index: Int) -> Token? { + var i = index + var scopeStack: [Token] = [] + while let token = formatter.tokenAtIndex(i) { + if let scope = scopeStack.last { + if token.closesScopeForToken(scope) { + scopeStack.popLast() + } + } else { + switch token.type { + case .Whitespace, .Linebreak: + break + case .StartOfScope: + if token.string == "/*" || token.string == "//" { + scopeStack.append(token) + } else { + return token + } + default: + return token + } + } + i += 1 + } + return nil + } + + func lastNonWhitespaceOrCommentOrLinebreak(fromIndex index: Int) -> Token? { + var i = index + var scopeStack: [Token] = [] + while let token = formatter.tokenAtIndex(i) { + if let scope = scopeStack.last { + if token.type == .StartOfScope && scope.closesScopeForToken(token) { + scopeStack.popLast() + } else { + return token + } + } else { + switch token.type { + case .Whitespace, .Linebreak: + break + case .EndOfScope: + if token.string == "*/" { + scopeStack.append(token) + } else { + return token + } + default: + return token + } + } + i -= 1 + } + return nil + } + + func currentScopeAtIndex(index: Int) -> Token? { + var i = index + var scopeStack: [Token] = [] + while let token = formatter.tokenAtIndex(i) { + if token.type == .StartOfScope { + if let scope = scopeStack.last where scope.closesScopeForToken(token) { + scopeStack.popLast() + } else { + return token + } + } else if token.type == .EndOfScope { + scopeStack.append(token) + } + i -= 1 + } + return nil + } + + func indentAtIndex(index: Int) -> Token? { + var i = index + while let token = formatter.tokenAtIndex(i) { + if token.type == .Linebreak { + break + } + i -= 1 + } + if let token = formatter.tokenAtIndex(i + 1) { + if token.type == .Whitespace { + return token + } + } + return nil + } + + formatter.forEachToken(";") { i, token in + if let nextToken = firstNonWhitespaceOrCommentOrLinebreak(fromIndex: i + 1) { + let lastToken = lastNonWhitespaceOrCommentOrLinebreak(fromIndex: i - 1) + if lastToken == nil || nextToken.string == "}" { + // Safe to remove + formatter.removeTokenAtIndex(i) + } else if lastToken?.string == "return" || currentScopeAtIndex(i)?.string == "(" { + // Not safe to remove or replace + } else if firstNonWhitespaceOrComment(fromIndex: i + 1)?.type == .Linebreak { + // Safe to remove + formatter.removeTokenAtIndex(i) + } else if !formatter.options.allowInlineSemicolons { + // Replace with a linebreak + if formatter.tokenAtIndex(i + 1)?.type == .Whitespace { + formatter.removeTokenAtIndex(i + 1) + } + if let indent = indentAtIndex(i) { + formatter.insertToken(indent, atIndex: i + 1) + } + formatter.replaceTokenAtIndex(i, with: Token(.Linebreak, "\n")) + } + } else { + // Safe to remove + formatter.removeTokenAtIndex(i) + } + } +} + public let defaultRules: [FormatRule] = [ + semicolons, + knrBraces, + elseOnSameLine, + indent, spaceAroundParens, spaceInsideParens, spaceAroundBrackets, @@ -967,9 +1133,6 @@ public let defaultRules: [FormatRule] = [ noTrailingWhitespace, noConsecutiveBlankLines, linebreakAtEndOfFile, - indent, - knrBraces, - elseOnSameLine, trailingCommas, todos, ] diff --git a/SwiftFormat/SwiftFormat.h b/SwiftFormat/SwiftFormat.h index f9606fcd..10a98eb2 100644 --- a/SwiftFormat/SwiftFormat.h +++ b/SwiftFormat/SwiftFormat.h @@ -2,7 +2,7 @@ // SwiftFormat // SwiftFormat.h // -// Version 0.3 +// Version 0.4 // // Created by Nick Lockwood on 12/08/2016. // Copyright 2016 Charcoal Design diff --git a/SwiftFormat/SwiftFormat.swift b/SwiftFormat/SwiftFormat.swift index bac72bd0..dbada6e7 100644 --- a/SwiftFormat/SwiftFormat.swift +++ b/SwiftFormat/SwiftFormat.swift @@ -2,7 +2,7 @@ // SwiftFormat // SwiftFormat.swift // -// Version 0.3 +// Version 0.4 // // Created by Nick Lockwood on 12/08/2016. // Copyright 2016 Charcoal Design @@ -37,15 +37,15 @@ import Foundation public func format(source: String, rules: [FormatRule] = defaultRules, options: FormattingOptions = FormattingOptions()) -> String { - + // Parse var tokens = tokenize(source) - + // Format let formatter = Formatter(tokens, options: options) rules.forEach { $0(formatter) } tokens = formatter.tokens - + // Output return tokens.reduce("", combine: { $0 + $1.string }) } diff --git a/SwiftFormat/Tokenizer.swift b/SwiftFormat/Tokenizer.swift index 9f3742f9..071e5dfe 100644 --- a/SwiftFormat/Tokenizer.swift +++ b/SwiftFormat/Tokenizer.swift @@ -2,7 +2,7 @@ // SwiftFormat // Tokenizer.swift // -// Version 0.3 +// Version 0.4 // // Created by Nick Lockwood on 11/08/2016. // Copyright 2016 Charcoal Design @@ -50,12 +50,12 @@ public enum TokenType { public struct Token: Equatable { public let type: TokenType public let string: String - + public init(_ type: TokenType, _ string: String) { self.type = type self.string = string } - + public var isWhitespaceOrComment: Bool { switch type { case .Whitespace, .CommentBody: @@ -68,11 +68,11 @@ public struct Token: Equatable { return false } } - + public var isWhitespaceOrCommentOrLinebreak: Bool { return type == .Linebreak || isWhitespaceOrComment } - + public func closesScopeForToken(token: Token) -> Bool { guard type != .StringBody && type != .CommentBody else { return false @@ -105,17 +105,17 @@ public func ==(lhs: Token, rhs: Token) -> Bool { } private extension Character { - + var unicodeValue: UInt32 { return String(self).unicodeScalars.first?.value ?? 0 } - + var isAlpha: Bool { return isalpha(Int32(unicodeValue)) > 0 } var isDigit: Bool { return isdigit(Int32(unicodeValue)) > 0 } } private extension String.CharacterView { - + mutating func scanCharacter(matching: (Character) -> Bool) -> String? { if let c = first where matching(c) { self = suffixFrom(startIndex.advancedBy(1)) @@ -123,7 +123,7 @@ private extension String.CharacterView { } return nil } - + mutating func scanString(matching string: String) -> String? { if startsWith(string.characters) { self = suffixFrom(startIndex.advancedBy(string.characters.count)) @@ -131,7 +131,7 @@ private extension String.CharacterView { } return nil } - + mutating func scanCharacters(matching: (Character) -> Bool) -> String? { var index = endIndex for (i, c) in enumerate() { @@ -147,49 +147,49 @@ private extension String.CharacterView { } return nil } - + mutating func scanInteger() -> String? { return scanCharacters({ $0.isDigit }) } } private extension String.CharacterView { - + mutating func parseToken(type: TokenType, _ character: Character) -> Token? { if let _ = scanCharacter({ $0 == character }) { return Token(type, String(character)) } return nil } - + mutating func parseToken(type: TokenType, _ string: String) -> Token? { if let string = scanString(matching: string) { return Token(type, string) } return nil } - + mutating func parseToken(type: TokenType, oneOf characters: String.CharacterView) -> Token? { if let string = scanCharacter({ characters.contains($0) }) { return Token(type, String(string)) } return nil } - + mutating func parseToken(type: TokenType, _ characters: String.CharacterView) -> Token? { if let string = scanCharacters({ characters.contains($0) }) { return Token(type, string) } return nil } - + mutating func parseToken(type: TokenType, upTo characters: String.CharacterView) -> Token? { if let string = scanCharacters({ !characters.contains($0) }) { return Token(type, string) } return nil } - + mutating func parseToken(type: TokenType, upTo character: Character) -> Token? { if let string = scanCharacters({ $0 != character }) { return Token(type, string) @@ -199,11 +199,11 @@ private extension String.CharacterView { } private extension String.CharacterView { - + mutating func parseWhitespace() -> Token? { return parseToken(.Whitespace, " \t".characters) // TODO: vertical tab } - + mutating func parseOperator() -> Token? { func isHead(c: Character) -> Bool { if "./=­-+!*%<>&|^~?".characters.contains(c) { @@ -230,7 +230,7 @@ private extension String.CharacterView { return false } } - + func isTail(c: Character) -> Bool { if isHead(c) { return true @@ -247,7 +247,7 @@ private extension String.CharacterView { return false } } - + if var tail = scanCharacter(isHead) { var head = "" while let c = scanCharacter(isTail) { @@ -274,19 +274,19 @@ private extension String.CharacterView { } return nil } - + mutating func parsePunctuation() -> Token? { return parseToken(.Operator, ":;,".characters) } - + mutating func parseStartOfScope() -> Token? { return parseToken(.StartOfScope, oneOf: "([{\"".characters) } - + mutating func parseEndOfScope() -> Token? { return parseToken(.EndOfScope, oneOf: "}])".characters) } - + mutating func parseIdentifier() -> Token? { func isHead(c: Character) -> Bool { if c.isAlpha || c == "_" || c == "$" { @@ -344,7 +344,7 @@ private extension String.CharacterView { return false } } - + func isTail(c: Character) -> Bool { if isHead(c) || c.isDigit { return true @@ -359,7 +359,7 @@ private extension String.CharacterView { return false } } - + func scanIdentifier() -> String? { if let head = scanCharacter({ isHead($0) || $0 == "@" || $0 == "#" }) { if let tail = scanCharacters({ isTail($0) }) { @@ -369,7 +369,7 @@ private extension String.CharacterView { } return nil } - + let start = self if scanCharacter({ $0 == "`" }) != nil { if let identifier = scanIdentifier() { @@ -389,7 +389,7 @@ private extension String.CharacterView { } return nil } - + mutating func parseNumber() -> Token? { var number = "" if let integer = scanInteger() { @@ -415,7 +415,7 @@ private extension String.CharacterView { } return nil } - + mutating func parseLineBreak() -> Token? { if scanCharacter({ $0 == "\r" }) != nil { if scanCharacter({ $0 == "\n" }) != nil { @@ -425,7 +425,7 @@ private extension String.CharacterView { } return parseToken(.Linebreak, "\n") } - + mutating func parseToken() -> Token? { // Have to split into groups for Swift to be able to process this if let token = parseWhitespace() ?? @@ -453,7 +453,7 @@ func tokenize(source: String) -> [Token] { var characters = source.characters var lastNonWhitespaceIndex: Int? var closedGenericScopeIndexes: [Int] = [] - + func processStringBody() { var string = "" var escaped = false @@ -487,7 +487,7 @@ func tokenize(source: String) -> [Token] { string += c } } - + func processCommentBody() { var comment = "" while let c = characters.scanCharacter({ _ in true }) { @@ -528,7 +528,7 @@ func tokenize(source: String) -> [Token] { comment += c } } - + func processToken() { let token = tokens.last! if token.type != .Whitespace { @@ -548,7 +548,7 @@ func tokenize(source: String) -> [Token] { case .StartOfScope: wasOperator = (token.string == "\"") case .Operator: - wasOperator = !["->", ">", ",", ":", ";", "?", "!", "."].contains(token.string) + wasOperator = !["=", "->", ">", ",", ":", ";", "?", "!", "."].contains(token.string) default: wasOperator = false } @@ -596,7 +596,7 @@ func tokenize(source: String) -> [Token] { processToken() return } - + } else if scopeIndexStack.last != nil && tokens[scopeIndexStack.last!].string == "\"" { processStringBody() } @@ -654,18 +654,18 @@ func tokenize(source: String) -> [Token] { } } } - + while let token = characters.parseToken() { tokens.append(token) processToken() } - + if let scopeIndex = scopeIndexStack.last where tokens[scopeIndex].string == "<" { // If we encountered an end-of-file while a generic scope was // still open, the opening < must have been an operator tokens[scopeIndex] = Token(.Operator, "<") scopeIndexStack.popLast() } - + return tokens } diff --git a/SwiftFormatTests/FormatterTests.swift b/SwiftFormatTests/FormatterTests.swift index 5f076a5c..3d39cab9 100644 --- a/SwiftFormatTests/FormatterTests.swift +++ b/SwiftFormatTests/FormatterTests.swift @@ -2,7 +2,7 @@ // SwiftFormat // FormatterTests.swift // -// Version 0.3 +// Version 0.4 // // Created by Nick Lockwood on 12/08/2016. // Copyright 2016 Charcoal Design @@ -35,851 +35,921 @@ import XCTest import SwiftFormat class FormatterTests: XCTestCase { - + // MARK: spaceAroundParens - + func testSpaceAfterSet() { let input = "private(set)var foo: Int" let output = "private(set) var foo: Int" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testSpaceBetweenParenAndClass() { let input = "@objc(XYZFoo)class foo" let output = "@objc(XYZFoo) class foo" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testSpaceBetweenParenAndAs() { let input = "(foo) as? String" let output = "(foo) as? String" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testNoSpaceAfterParenAtEndOfFile() { let input = "(foo)" let output = "(foo)" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testSpaceBetweenParenAndFoo() { let input = "func foo ()" let output = "func foo()" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testNoSpaceBetweenParenAndInit() { let input = "init ()" let output = "init()" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testNoSpaceBetweenObjcAndSelector() { let input = "@objc (XYZFoo) class foo" let output = "@objc(XYZFoo) class foo" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testSpaceBetweenPrivateAndSet() { let input = "private (set) var foo: Int" let output = "private(set) var foo: Int" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testSpaceBetweenIfAndCondition() { let input = "if(true) {}" let output = "if (true) {}" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testNoSpaceBetweenArrayLiteralAndParen() { let input = "[String] ()" let output = "[String]()" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testSpaceBetweenClosingParenAndOpenBrace() { let input = "func foo(){foo}" let output = "func foo() {foo}" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testNoSpaceBetweenClosingBraceAndParens() { let input = "{ block } ()" let output = "{ block }()" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + func testDontRemoveSpaceBetweenOpeningBraceAndParens() { let input = "a = (b + c)" let output = "a = (b + c)" XCTAssertEqual(format(input, rules: [spaceAroundParens]), output) } - + // MARK: spaceInsideParens - + func testSpaceInsideParens() { let input = "( 1, ( 2, 3 ) )" let output = "(1, (2, 3))" XCTAssertEqual(format(input, rules: [spaceInsideParens]), output) } - + // MARK: spaceAroundBrackets - + func testSubscriptSpacing() { let input = "foo[bar] = baz" let output = "foo[bar] = baz" XCTAssertEqual(format(input, rules: [spaceAroundBrackets]), output) } - + func testArrayLiteralSpacing() { let input = "foo = [bar, baz]" let output = "foo = [bar, baz]" XCTAssertEqual(format(input, rules: [spaceAroundBrackets]), output) } - + func testAsArrayCasting() { let input = "foo as[String]" let output = "foo as [String]" XCTAssertEqual(format(input, rules: [spaceAroundBrackets]), output) } - + func testAsOptionalArrayCasting() { let input = "foo as? [String]" let output = "foo as? [String]" XCTAssertEqual(format(input, rules: [spaceAroundBrackets]), output) } - + func testIsArrayTesting() { let input = "if foo is[String]" let output = "if foo is [String]" XCTAssertEqual(format(input, rules: [spaceAroundBrackets]), output) } - + // MARK: spaceInsideBrackets - + func testSpaceInsideBrackets() { let input = "foo[ 5 ]" let output = "foo[5]" XCTAssertEqual(format(input, rules: [spaceInsideBrackets]), output) } - + // MARK: spaceAroundBraces - + func testSpaceAroundTrailingClosure() { let input = "if x{y}else{z}" let output = "if x {y} else {z}" XCTAssertEqual(format(input, rules: [spaceAroundBraces]), output) } - + func testNoSpaceAroundClosureInsiderParens() { let input = "foo({ $0 == 5 })" let output = "foo({ $0 == 5 })" XCTAssertEqual(format(input, rules: [spaceAroundBraces]), output) } - + func testNoExtraSpaceAroundBracesAtStartOrEndOfFile() { let input = "{foo}" let output = "{foo}" XCTAssertEqual(format(input, rules: [spaceAroundBraces]), output) } - + func testSpaceAroundBracesAfterOptionalProperty() { let input = "var: Foo?{}" let output = "var: Foo? {}" XCTAssertEqual(format(input, rules: [spaceAroundBraces]), output) } - + func testSpaceAroundBracesAfterImplicitlyUnwrappedProperty() { let input = "var: Foo!{}" let output = "var: Foo! {}" XCTAssertEqual(format(input, rules: [spaceAroundBraces]), output) } - + func testSpaceAroundBracesAfterNumber() { let input = "if x = 5{}" let output = "if x = 5 {}" XCTAssertEqual(format(input, rules: [spaceAroundBraces]), output) } - + func testSpaceAroundBracesAfterString() { let input = "if x = \"\"{}" let output = "if x = \"\" {}" XCTAssertEqual(format(input, rules: [spaceAroundBraces]), output) } - + // MARK: spaceInsideBraces - + func testSpaceInsideBraces() { let input = "foo({bar})" let output = "foo({ bar })" XCTAssertEqual(format(input, rules: [spaceInsideBraces]), output) } - + func testNoExtraSpaceInsidebraces() { let input = "{ foo }" let output = "{ foo }" XCTAssertEqual(format(input, rules: [spaceInsideBraces]), output) } - + func testNoSpaceInsideEmptybraces() { let input = "foo({ })" let output = "foo({})" XCTAssertEqual(format(input, rules: [spaceInsideBraces]), output) } - + // MARK: spaceAroundGenerics - + func testSpaceAroundGenerics() { let input = "Foo >" let output = "Foo>" XCTAssertEqual(format(input, rules: [spaceAroundGenerics]), output) } - + // MARK: spaceInsideGenerics - + func testSpaceInsideGenerics() { let input = "Foo< Bar< Baz > >" let output = "Foo>" XCTAssertEqual(format(input, rules: [spaceInsideGenerics]), output) } - + // MARK: spaceAroundOperators - + func testSpaceAfterColon() { let input = "let foo:Bar = 5" let output = "let foo: Bar = 5" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceAfterComma() { let input = "let foo = [1,2,3]" let output = "let foo = [1, 2, 3]" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceBetweenColonAndEnumValue() { let input = "[.Foo:.Bar]" let output = "[.Foo: .Bar]" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceBetweenCommaAndEnumValue() { let input = "[.Foo,.Bar]" let output = "[.Foo, .Bar]" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceBetweenSemicolonAndEnumValue() { let input = "statement;.Bar" let output = "statement; .Bar" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceBetweenEqualsAndEnumValue() { let input = "foo = .Bar" let output = "foo = .Bar" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testNoSpaceBeforeColon() { let input = "let foo : Bar = 5" let output = "let foo: Bar = 5" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceBeforeColonInTernary() { let input = "foo ? bar : baz" let output = "foo ? bar : baz" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testTernaryOfEnumValues() { let input = "foo ? .Bar : .Baz" let output = "foo ? .Bar : .Baz" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceBeforeColonInNestedTernary() { let input = "foo ? (hello + a ? b: c) : baz" let output = "foo ? (hello + a ? b : c) : baz" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testNoSpaceBeforeComma() { let input = "let foo = [1 , 2 , 3]" let output = "let foo = [1, 2, 3]" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceAtStartOfLine() { let input = "foo\n ,bar" let output = "foo\n , bar" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceAroundInfixMinus() { let input = "foo-bar" let output = "foo - bar" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testNoSpaceAroundPrefixMinus() { let input = "foo + -bar" let output = "foo + -bar" XCTAssertEqual(format(input, rules: [spaceAroundOperators]), output) } - + func testSpaceAroundLessThan() { let input = "foo(first: [T], _ second: [T]) { } class TokenizerTests: XCTestCase { - + // MARK: Strings - + func testEmptyString() { let input = "\"\"" let output = [ @@ -50,7 +50,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testSimpleString() { let input = "\"foo\"" let output = [ @@ -60,7 +60,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testStringWithEscape() { let input = "\"hello\\tworld\"" let output = [ @@ -70,7 +70,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testStringWithEscapedQuotes() { let input = "\"\\\"nice\\\" to meet you\"" let output = [ @@ -80,7 +80,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testStringWithEscapedLogic() { let input = "\"hello \\(name)\"" let output = [ @@ -93,7 +93,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testStringWithEscapedBackslash() { let input = "\"\\\\\"" let output = [ @@ -103,9 +103,9 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + // MARK: Single-line comments - + func testSingleLineComment() { let input = "//foo" let output = [ @@ -114,7 +114,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testSingleLineCommentWithSpace() { let input = "// foo" let output = [ @@ -124,7 +124,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testSingleLineCommentWithLinebreak() { let input = "//foo\nbar" let output = [ @@ -135,9 +135,9 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + // MARK: Multiline comments - + func testSingleLineMultilineComment() { let input = "/*foo*/" let output = [ @@ -147,7 +147,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testSingleLineMultilineCommentWithSpace() { let input = "/* foo*/" let output = [ @@ -158,7 +158,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testMultilineComment() { let input = "/*foo\nbar*/" let output = [ @@ -170,7 +170,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testMultilineCommentWithWhitespace() { let input = "/*foo\n bar*/" let output = [ @@ -183,7 +183,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testNestedComments() { let input = "/*foo/*bar*/baz*/" let output = [ @@ -197,27 +197,27 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + // MARK: Numbers - + func testZero() { let input = "0" let output = [Token(.Number, "0")] XCTAssertEqualArrays(tokenize(input), output) } - + func testSmallInteger() { let input = "5" let output = [Token(.Number, "5")] XCTAssertEqualArrays(tokenize(input), output) } - + func testLargeInteger() { let input = "12345678901234567890" let output = [Token(.Number, "12345678901234567890")] XCTAssertEqualArrays(tokenize(input), output) } - + func testNegativeInteger() { let input = "-7" let output = [ @@ -226,19 +226,19 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testSmallFloat() { let input = "0.2" let output = [Token(.Number, "0.2")] XCTAssertEqualArrays(tokenize(input), output) } - + func testLargeFloat() { let input = "1234.567890" let output = [Token(.Number, "1234.567890")] XCTAssertEqualArrays(tokenize(input), output) } - + func testNegativeFloat() { let input = "-0.34" let output = [ @@ -247,114 +247,114 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testExponential() { let input = "1234e5" let output = [Token(.Number, "1234e5")] XCTAssertEqualArrays(tokenize(input), output) } - + func testPositiveExponential() { let input = "0.123e+4" let output = [Token(.Number, "0.123e+4")] XCTAssertEqualArrays(tokenize(input), output) } - + func testNegativeExponential() { let input = "0.123e-4" let output = [Token(.Number, "0.123e-4")] XCTAssertEqualArrays(tokenize(input), output) } - + func testCapitalExponential() { let input = "0.123E-4" let output = [Token(.Number, "0.123E-4")] XCTAssertEqualArrays(tokenize(input), output) } - + // MARK: Identifiers - + func testFoo() { let input = "foo" let output = [Token(.Identifier, "foo")] XCTAssertEqualArrays(tokenize(input), output) } - + func testDollar0() { let input = "$0" let output = [Token(.Identifier, "$0")] XCTAssertEqualArrays(tokenize(input), output) } - + func testDollar() { // Note: support for this is deprecated in Swift 3 let input = "$" let output = [Token(.Identifier, "$")] XCTAssertEqualArrays(tokenize(input), output) } - + func testFooDollar() { let input = "foo$" let output = [Token(.Identifier, "foo$")] XCTAssertEqualArrays(tokenize(input), output) } - + func test_() { let input = "_" let output = [Token(.Identifier, "_")] XCTAssertEqualArrays(tokenize(input), output) } - + func test_foo() { let input = "_foo" let output = [Token(.Identifier, "_foo")] XCTAssertEqualArrays(tokenize(input), output) } - + func testFoo_bar() { let input = "foo_bar" let output = [Token(.Identifier, "foo_bar")] XCTAssertEqualArrays(tokenize(input), output) } - + func testAtFoo() { let input = "@foo" let output = [Token(.Identifier, "@foo")] XCTAssertEqualArrays(tokenize(input), output) } - + func testHashFoo() { let input = "#foo" let output = [Token(.Identifier, "#foo")] XCTAssertEqualArrays(tokenize(input), output) } - + func testUnicode() { let input = "µsec" let output = [Token(.Identifier, "µsec")] XCTAssertEqualArrays(tokenize(input), output) } - + func testEmoji() { let input = "💩" let output = [Token(.Identifier, "💩")] XCTAssertEqualArrays(tokenize(input), output) } - + func testBacktickEscapedClass() { let input = "`class`" let output = [Token(.Identifier, "`class`")] XCTAssertEqualArrays(tokenize(input), output) } - + // MARK: Operators - + func testBasicOperator() { let input = "+=" let output = [Token(.Operator, "+=")] XCTAssertEqualArrays(tokenize(input), output) } - + func testDivide() { let input = "a / b" let output = [ @@ -366,13 +366,13 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testCustomOperator() { let input = "~=" let output = [Token(.Operator, "~=")] XCTAssertEqualArrays(tokenize(input), output) } - + func testSequentialOperators() { let input = "a *= -b" let output = [ @@ -385,19 +385,19 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testDotPrefixedOperator() { let input = "..." let output = [Token(.Operator, "...")] XCTAssertEqualArrays(tokenize(input), output) } - + func testUnicodeOperator() { let input = "≥" let output = [Token(.Operator, "≥")] XCTAssertEqualArrays(tokenize(input), output) } - + func testOperatorFollowedByComment() { let input = "a +/* b */" let output = [ @@ -411,7 +411,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testOperatorPrecededByComment() { let input = "/* a */-b" let output = [ @@ -424,9 +424,9 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + // MARK: chevrons (might be operators or generics) - + func testLessThanGreaterThan() { let input = "ac" let output = [ @@ -442,7 +442,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testBitshift() { let input = "a>>b" let output = [ @@ -452,7 +452,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testTripleShift() { let input = "a>>>b" let output = [ @@ -462,7 +462,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testTripleShiftEquals() { let input = "a>>=b" let output = [ @@ -472,7 +472,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testBitshiftThatLooksLikeAGeneric() { let input = "a>e" let output = [ @@ -492,7 +492,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testBasicGeneric() { let input = "Foo" let output = [ @@ -506,7 +506,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testNestedGenerics() { let input = "Foo>" let output = [ @@ -520,7 +520,7 @@ class TokenizerTests: XCTestCase { ] XCTAssertEqualArrays(tokenize(input), output) } - + func testFunctionThatLooksLikeGenericType() { let input = "y"), + Token(.Whitespace, " "), + Token(.Operator, "="), + Token(.Whitespace, " "), + Token(.Number, "5"), + ] + XCTAssertEqualArrays(tokenize(input), output) + } }