From 535afe4984a97da1a9346056f43418ddb1de4efc Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Sun, 16 Aug 2020 00:43:43 -0700 Subject: [PATCH] Add organizeDeclarations rule (#701) --- Rules.md | 63 ++++ Sources/Examples.swift | 45 +++ Sources/Options.swift | 18 ++ Sources/OptionsDescriptor.swift | 74 +++++ Sources/ParsingHelpers.swift | 213 ++++++++++++++ Sources/Rules.swift | 406 +++++++++++++++++++++++++- Sources/SwiftFormat.swift | 13 +- Sources/Tokenizer.swift | 34 +++ Tests/FormatterTests.swift | 256 +++++++++++++++++ Tests/RulesTests.swift | 491 +++++++++++++++++++++++++++++++- Tests/XCTestManifests.swift | 15 + 11 files changed, 1625 insertions(+), 3 deletions(-) diff --git a/Rules.md b/Rules.md index 3a8962627..f8970648e 100644 --- a/Rules.md +++ b/Rules.md @@ -23,6 +23,7 @@ * [multilineEnumCases](#multilineEnumCases) * [multilineSwitchCases](#multilineSwitchCases) * [numberFormatting](#numberFormatting) +* [organizeDeclarations](#organizeDeclarations) * [preferKeyPath](#preferKeyPath) * [ranges *(deprecated)*](#ranges) * [redundantBackticks](#redundantBackticks) @@ -654,6 +655,68 @@ Option | Description
+## organizeDeclarations + +Organizes declarations within class, struct, and enum bodies. + +Option | Description +--- | --- +`--categorymark` | Template for category mark comments (defaults to `MARK: %c`) +`--beforemarks` | Declarations placed before first mark (e.g. `typealias,struct`) +`--lifecycle` | Names of additional Lifecycle methods (e.g. `viewDidLoad`) +`--structthreshold` | Minimum line count to organize struct body (defaults to `nil`) +`--classthreshold` | Minimum line count to organize class body (defaults to `nil`) +`--enumthreshold` | Minimum line count to organize enum body (defaults to `nil`) + +
+Examples + +```diff + public class Foo { +- public func c() -> String {} +- +- public let a: Int = 1 +- private let g: Int = 2 +- let e: Int = 2 +- public let b: Int = 3 +- +- public func d() {} +- func f() {} +- init() {} +- deinit() {} + } + + public class Foo { ++ ++ // MARK: Lifecycle ++ ++ init() {} ++ deinit() {} ++ ++ // MARK: Public ++ ++ public let a: Int = 1 ++ public let b: Int = 3 ++ ++ public func c() -> String {} ++ public func d() {} ++ ++ // MARK: Internal ++ ++ let e: Int = 2 ++ ++ func f() {} ++ ++ // MARK: Private ++ ++ private let g: Int = 2 ++ + } +``` + +
+
+ ## preferKeyPath Convert trivial `map { $0.foo }` closures to keyPath-based syntax. diff --git a/Sources/Examples.swift b/Sources/Examples.swift index 9d10d83c7..124923c02 100644 --- a/Sources/Examples.swift +++ b/Sources/Examples.swift @@ -1128,4 +1128,49 @@ private struct Examples { + let barArray = fooArray.map(\\.bar) ``` """ + + let organizeDeclarations = """ + ```diff + public class Foo { + - public func c() -> String {} + - + - public let a: Int = 1 + - private let g: Int = 2 + - let e: Int = 2 + - public let b: Int = 3 + - + - public func d() {} + - func f() {} + - init() {} + - deinit() {} + } + + public class Foo { + + + + // MARK: Lifecycle + + + + init() {} + + deinit() {} + + + + // MARK: Public + + + + public let a: Int = 1 + + public let b: Int = 3 + + + + public func c() -> String {} + + public func d() {} + + + + // MARK: Internal + + + + let e: Int = 2 + + + + func f() {} + + + + // MARK: Private + + + + private let g: Int = 2 + + + } + ``` + """ } diff --git a/Sources/Options.swift b/Sources/Options.swift index 12da4c29e..69bcbaafc 100644 --- a/Sources/Options.swift +++ b/Sources/Options.swift @@ -308,6 +308,12 @@ public struct FormatOptions: CustomStringConvertible { public var funcAttributes: AttributeMode public var typeAttributes: AttributeMode public var varAttributes: AttributeMode + public var categoryMarkComment: String + public var beforeMarks: Set + public var lifecycleMethods: Set + public var organizeClassThreshold: Int? + public var organizeStructThreshold: Int? + public var organizeEnumThreshold: Int? public var yodaSwap: YodaMode // Deprecated @@ -369,6 +375,12 @@ public struct FormatOptions: CustomStringConvertible { funcAttributes: AttributeMode = .preserve, typeAttributes: AttributeMode = .preserve, varAttributes: AttributeMode = .preserve, + categoryMarkComment: String = "MARK: %c", + beforeMarks: Set = [], + lifecycleMethods: Set = [], + organizeClassThreshold: Int? = nil, + organizeStructThreshold: Int? = nil, + organizeEnumThreshold: Int? = nil, yodaSwap: YodaMode = .always, // Doesn't really belong here, but hard to put elsewhere fragment: Bool = false, @@ -423,6 +435,12 @@ public struct FormatOptions: CustomStringConvertible { self.funcAttributes = funcAttributes self.typeAttributes = typeAttributes self.varAttributes = varAttributes + self.categoryMarkComment = categoryMarkComment + self.beforeMarks = beforeMarks + self.lifecycleMethods = lifecycleMethods + self.organizeClassThreshold = organizeClassThreshold + self.organizeStructThreshold = organizeStructThreshold + self.organizeEnumThreshold = organizeEnumThreshold self.yodaSwap = yodaSwap // Doesn't really belong here, but hard to put elsewhere self.fragment = fragment diff --git a/Sources/OptionsDescriptor.swift b/Sources/OptionsDescriptor.swift index b0e411c9a..fd827e21d 100644 --- a/Sources/OptionsDescriptor.swift +++ b/Sources/OptionsDescriptor.swift @@ -295,6 +295,12 @@ extension FormatOptions.Descriptor { funcAttributes, typeAttributes, varAttributes, + categoryMark, + beforeMarks, + lifecycleMethods, + organizeClassThreshold, + organizeStructThreshold, + organizeEnumThreshold, yodaSwap, // Deprecated @@ -684,6 +690,74 @@ extension FormatOptions.Descriptor { keyPath: \.shortOptionals, options: ["always", "except-properties"] ) + static let categoryMark = FormatOptions.Descriptor( + argumentName: "categorymark", + propertyName: "categoryMarkComment", + displayName: "Category Mark Comment", + help: "Template for category mark comments (defaults to `MARK: %c`)", + keyPath: \.categoryMarkComment, + fromArgument: { $0 }, + toArgument: { $0 } + ) + static let beforeMarks = FormatOptions.Descriptor( + argumentName: "beforemarks", + propertyName: "beforeMarks", + displayName: "Before Marks", + help: "Declarations placed before first mark (e.g. `typealias,struct`)", + keyPath: \.beforeMarks + ) + static let lifecycleMethods = FormatOptions.Descriptor( + argumentName: "lifecycle", + propertyName: "lifecycleMethods", + displayName: "Lifecycle Methods", + help: "Names of additional Lifecycle methods (e.g. `viewDidLoad`)", + keyPath: \.lifecycleMethods + ) + static let organizeStructThreshold = FormatOptions.Descriptor( + argumentName: "structthreshold", + propertyName: "organizeStructThreshold", + displayName: "Organize Struct Threshold", + help: "Minimum line count to organize struct body (defaults to `nil`)", + keyPath: \.organizeStructThreshold, + fromArgument: { .some(Int($0)) }, + toArgument: { + if let lineCount = $0 { + return "\(lineCount)" + } else { + return "" + } + } + ) + static let organizeClassThreshold = FormatOptions.Descriptor( + argumentName: "classthreshold", + propertyName: "organizeClassThreshold", + displayName: "Organize Class Threshold", + help: "Minimum line count to organize class body (defaults to `nil`)", + keyPath: \.organizeClassThreshold, + fromArgument: { .some(Int($0)) }, + toArgument: { + if let lineCount = $0 { + return "\(lineCount)" + } else { + return "" + } + } + ) + static let organizeEnumThreshold = FormatOptions.Descriptor( + argumentName: "enumthreshold", + propertyName: "organizeEnumThreshold", + displayName: "Organize Enum Threshold", + help: "Minimum line count to organize enum body (defaults to `nil`)", + keyPath: \.organizeEnumThreshold, + fromArgument: { .some(Int($0)) }, + toArgument: { + if let lineCount = $0 { + return "\(lineCount)" + } else { + return "" + } + } + ) // MARK: - Internal diff --git a/Sources/ParsingHelpers.swift b/Sources/ParsingHelpers.swift index 30e65f6c3..277c10ea2 100644 --- a/Sources/ParsingHelpers.swift +++ b/Sources/ParsingHelpers.swift @@ -928,6 +928,219 @@ extension Formatter { return importStack } + public indirect enum Declaration: Equatable { + /// A Type of type-like declaration with body of additional declarations (`class`, `struct`, etc) + case type(kind: String, open: [Token], body: [Declaration], close: [Token]) + + /// A simple declaration (like a property or function) + case declaration(kind: String, tokens: [Token]) + + /// The tokens in this declaration + public var tokens: [Token] { + switch self { + case let .declaration(_, tokens): + return tokens + case let .type(_, openTokens, bodyDeclarations, closeTokens): + return openTokens + bodyDeclarations.flatMap { $0.tokens } + closeTokens + } + } + + /// The body of this declaration, if applicable + public var body: [Declaration]? { + switch self { + case .declaration: + return nil + case let .type(_, _, body, _): + return body + } + } + + /// The keyword that determines the specific type of declaration that this is + /// (`class`, `func`, `let`, `var`, etc.) + public var keyword: String { + switch self { + case let .declaration(kind, _), + let .type(kind, _, _, _): + return kind + } + } + } + + public func parseDeclarations() -> [Declaration] { + let parser = Formatter(tokens) + var declarations = [Declaration]() + + while !parser.tokens.isEmpty { + let startOfDeclaration = 0 + var endOfDeclaration: Int? + + // Determine what type of declaration this is + let declarationTypeKeywordIndex: Int? + let declarationKeyword: String? + + if let firstDeclarationTypeKeywordIndex = parser.index( + after: startOfDeclaration - 1, + where: { $0.definesDeclarationType } + ) { + // Most declarations will include exactly one token that `definesDeclarationType` in + // their outermost scope, but `class func` methods will have two (and the first one will be incorrect!) + if parser.tokens[firstDeclarationTypeKeywordIndex].string != "class" { + declarationTypeKeywordIndex = firstDeclarationTypeKeywordIndex + declarationKeyword = parser.tokens[firstDeclarationTypeKeywordIndex].string + } + + // For `class` declarations, we have to look at the _last_ token that + // `definesDeclarationType` in the declaration's opening sequence (up until the `{`). + // - This makes sure that we correctly identify `class func` declarations as being functions. + else if let endOfDeclarationOpeningSequence = parser.index(after: -1, where: { $0 == .startOfScope("{") }), + let lastDeclarationTypeKeywordIndex = parser.lastIndex( + in: 0 ..< endOfDeclarationOpeningSequence, + where: { $0.definesDeclarationType } + ) + { + declarationTypeKeywordIndex = lastDeclarationTypeKeywordIndex + declarationKeyword = parser.tokens[lastDeclarationTypeKeywordIndex].string + } else { + declarationTypeKeywordIndex = nil + declarationKeyword = nil + } + } else { + declarationTypeKeywordIndex = nil + declarationKeyword = nil + } + + if let declarationTypeKeywordIndex = declarationTypeKeywordIndex { + // Search for the next declaration so we know where this declaration ends. + var nextDeclarationKeywordIndex: Int? + var searchIndex = declarationTypeKeywordIndex + 1 + + while searchIndex < parser.tokens.count, nextDeclarationKeywordIndex == nil { + // If we encounter a `startOfScope`, we have to skip to the end of the scope. + // This accounts for things like function bodies, etc. + if parser.tokens[searchIndex].isStartOfScope, + let endOfScope = parser.endOfScope(at: searchIndex) + { + searchIndex = endOfScope + 1 + } else if parser.tokens[searchIndex].definesDeclarationType { + nextDeclarationKeywordIndex = searchIndex + } else { + searchIndex += 1 + } + } + + if let nextDeclarationKeywordIndex = nextDeclarationKeywordIndex { + // Search backward from the next declaration's type keyword + // to find exactly where that declaration begins. + var startOfNextDeclaration: Int? + searchIndex = nextDeclarationKeywordIndex + + while searchIndex > declarationTypeKeywordIndex, startOfNextDeclaration == nil { + if parser.tokens[searchIndex - 1].canPrecedeDeclarationTypeKeyword { + searchIndex -= 1 + } + + // If we encounter an `endOfScope`, we have to skip to the beginning of the scope. + // This accounts for things like attribute bodies, etc. + else if parser.tokens[searchIndex - 1].isEndOfScope { + let encounteredEndOfScope = searchIndex - 1 + var startOfScope: Int? + searchIndex -= 1 + + while searchIndex > declarationTypeKeywordIndex, startOfScope == nil { + if parser.tokens[searchIndex].isStartOfScope, + parser.endOfScope(at: searchIndex) == encounteredEndOfScope + { + startOfScope = searchIndex + // Confirm whether or not this scope should be grouped with the + // current or previous declaration: + if let previousNonwhitespace = parser.index( + of: .nonSpaceOrCommentOrLinebreak, + before: searchIndex + ), + !parser.tokens[previousNonwhitespace].canPrecedeDeclarationTypeKeyword + { + startOfNextDeclaration = encounteredEndOfScope + 1 + } + + } else { + searchIndex -= 1 + } + } + + } else { + startOfNextDeclaration = searchIndex + } + } + + // Now that we know where the next declaration starts, + // we know where this declaration ends. + if let startOfNextDeclaration = startOfNextDeclaration { + endOfDeclaration = startOfNextDeclaration - 1 + } + } + } + + // Prefer keeping linebreaks at the end of a declaration's tokens, + // instead of the start of the next delaration's tokens + while let linebreakSearchIndex = endOfDeclaration, + parser.token(at: linebreakSearchIndex + 1)?.isLinebreak == true + { + endOfDeclaration = linebreakSearchIndex + 1 + } + + let declarationRange = startOfDeclaration ... (endOfDeclaration ?? parser.tokens.count - 1) + let declaration = Array(parser.tokens[declarationRange]) + parser.removeTokens(inRange: declarationRange) + + declarations.append(.declaration(kind: declarationKeyword ?? "unknown", tokens: declaration)) + } + + return declarations.map { declaration in + let declarationParser = Formatter(declaration.tokens) + + // If this declaration represents a type, we need to parse its inner declarations as well. + let typelikeKeywords = ["class", "struct", "enum", "protocol", "extension"] + + if typelikeKeywords.contains(declaration.keyword), + let declarationTypeKeywordIndex = declarationParser.index( + after: -1, + where: { $0.string == declaration.keyword } + ), + let startOfBody = declarationParser.index(of: .startOfScope("{"), after: declarationTypeKeywordIndex), + let endOfBody = declarationParser.endOfScope(at: startOfBody) + { + var startTokens = Array(declarationParser.tokens[...startOfBody]) + var bodyTokens = Array(declarationParser.tokens[startOfBody + 1 ..< endOfBody]) + var endTokens = Array(declarationParser.tokens[endOfBody...]) + + // Move the leading newlines from the `body` into the `start` tokens + // so the first body token is the start of the first declaration + while bodyTokens.first?.isLinebreak == true { + startTokens.append(bodyTokens[0]) + bodyTokens = Array(bodyTokens.dropFirst()) + } + + // Move the closing brace's indentation token from the `body` into the `end` tokens + if let lastBodyToken = bodyTokens.last, lastBodyToken.isSpace { + endTokens.insert(lastBodyToken, at: 0) + bodyTokens = Array(bodyTokens.dropLast()) + } + + // Parse the inner body declarations of the type + let bodyDeclarations = Formatter(bodyTokens).parseDeclarations() + + return .type( + kind: declaration.keyword, + open: startTokens, + body: bodyDeclarations, + close: endTokens + ) + } else { + return declaration + } + } + } + // Shared wrap implementation func wrapCollectionsAndArguments(completePartialWrapping: Bool, wrapSingleArguments: Bool) { let maxWidth = options.maxWidth diff --git a/Sources/Rules.swift b/Sources/Rules.swift index 096a47db2..312a0abcd 100644 --- a/Sources/Rules.swift +++ b/Sources/Rules.swift @@ -39,6 +39,7 @@ public final class FormatRule: Equatable, Comparable { let orderAfter: [String] let options: [String] let sharedOptions: [String] + let rewritesEntireFile: Bool var deprecationMessage: String? { return FormatRule.deprecatedMessage[name] @@ -52,6 +53,7 @@ public final class FormatRule: Equatable, Comparable { orderAfter: [String] = [], options: [String] = [], sharedOptions: [String] = [], + rewritesEntireFile: Bool = false, _ fn: @escaping (Formatter) -> Void) { self.fn = fn @@ -59,6 +61,7 @@ public final class FormatRule: Equatable, Comparable { self.orderAfter = orderAfter self.options = options self.sharedOptions = sharedOptions + self.rewritesEntireFile = rewritesEntireFile } public func apply(with formatter: Formatter) { @@ -124,7 +127,7 @@ private func allRules(except rules: [String]) -> [FormatRule] { private let _allRules = allRules(except: []) private let _defaultRules = allRules(except: _disabledByDefault) private let _deprecatedRules = FormatRule.deprecatedMessage.keys -private let _disabledByDefault = _deprecatedRules + ["isEmpty"] +private let _disabledByDefault = _deprecatedRules + ["isEmpty", "organizeDeclarations"] public extension _FormatRules { /// A Dictionary of rules by name @@ -4842,4 +4845,405 @@ public struct _FormatRules { formatter.replaceTokens(inRange: prevIndex + 1 ... endIndex, with: replacementTokens) } } + + public let organizeDeclarations = FormatRule( + help: "Organizes declarations within class, struct, and enum bodies.", + options: ["categorymark", "beforemarks", "lifecycle", "structthreshold", "classthreshold", "enumthreshold"], + rewritesEntireFile: true + ) { formatter in + /// Categories of declarations within an individual type + enum Category: String, CaseIterable { + case beforeMarks + case lifecycle + case open + case `public` + case `internal` + case `fileprivate` + case `private` + + /// The comment tokens that should preceed all declarations in this category + func markComment(from template: String) -> String? { + switch self { + case .beforeMarks: + return nil + default: + return "// \(template.replacingOccurrences(of: "%c", with: rawValue.capitalized))" + } + } + } + + /// Types of declarations that can be present within an individual category + enum DeclarationType { + case nestedType + case staticProperty + case staticPropertyWithBody + case instanceProperty + case instancePropertyWithBody + case staticMethod + case classMethod + case instanceMethod + } + + let categoryOrdering: [Category] = [ + .beforeMarks, .lifecycle, .open, .public, .internal, .fileprivate, .private, + ] + + let categorySubordering: [DeclarationType] = [ + .nestedType, .staticProperty, .staticPropertyWithBody, .instanceProperty, + .instancePropertyWithBody, .staticMethod, .classMethod, .instanceMethod, + ] + + /// The `Category` of the given `Declaration` + func category(of declaration: Formatter.Declaration) -> Category { + switch declaration { + case let .declaration(keyword, tokens), let .type(keyword, open: tokens, _, _): + let parser = Formatter(tokens) + + // Enum cases don't fit into any of the other categories, + // so they should go in the intial top section. + // - The user can also provide other declaration types to place in this category + if declaration.keyword == "case" || formatter.options.beforeMarks.contains(declaration.keyword) { + return .beforeMarks + } + + if categoryOrdering.contains(.lifecycle) { + // `init` and `deinit` always go in Lifecycle if it's present + if tokens.contains(.keyword("init")) || tokens.contains(.keyword("deinit")) { + return .lifecycle + } + + // The user can also provide specific instance method names to place in Lifecycle + // - In the function declaration grammar, the function name always + // immediately follows the `func` keyword: + // https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#grammar_function-name + if keyword == "func", + let funcKeywordIndex = parser.index( + after: -1, + where: { $0.definesDeclarationType && $0.string == keyword } + ), + let methodName = parser.next(.nonSpaceOrCommentOrLinebreak, after: funcKeywordIndex), + formatter.options.lifecycleMethods.contains(methodName.string) + { + return .lifecycle + } + } + + // Search for a visibility keyword, + // making sure we exclude groups like private(set) + var searchIndex = 0 + + while searchIndex < parser.tokens.count { + if parser.tokens[searchIndex].isKeyword, + let visibilityCategory = Category(rawValue: parser.tokens[searchIndex].string), + parser.next(.nonSpaceOrComment, after: searchIndex) != .startOfScope("(") + { + return visibilityCategory + } + + searchIndex += 1 + } + + // `internal` is the default implied vibilility if no other is specified + return .internal + } + } + + /// The `DeclarationType` of the given `Declaration` + func type(of declaration: Formatter.Declaration) -> DeclarationType? { + switch declaration { + case .type: + return .nestedType + + case let .declaration(keyword, tokens): + let declarationParser = Formatter(tokens) + + guard let declarationTypeTokenIndex = declarationParser.index( + after: -1, + where: { $0.isKeyword && $0.string == keyword } + ) + else { return nil } + + let declarationTypeToken = declarationParser.tokens[declarationTypeTokenIndex] + + let isStaticDeclaration = declarationParser.lastToken( + before: declarationTypeTokenIndex, + where: { $0 == .keyword("static") } + ) != nil + + let isClassDeclaration = declarationParser.lastToken( + before: declarationTypeTokenIndex, + where: { $0 == .keyword("class") } + ) != nil + + let hasBody: Bool + // If there's an opening bracket and no equals operator, + // then this declaration has a body (e.g. a function body or a computed property body) + if let openingBraceIndex = declarationParser.index( + after: declarationTypeTokenIndex, + where: { $0 == .startOfScope("{") } + ) { + hasBody = declarationParser.index( + of: .operator("=", .infix), + in: CountableRange(declarationTypeTokenIndex ... openingBraceIndex) + ) == nil + } else { + hasBody = false + } + + switch declarationTypeToken { + // Properties and property-like declarations + case .keyword("let"), .keyword("var"), .keyword("typealias"), + .keyword("case"), .keyword("operator"), .keyword("precedencegroup"): + if isStaticDeclaration { + if hasBody { + return .staticPropertyWithBody + } else { + return .staticProperty + } + } else { + if hasBody { + return .instancePropertyWithBody + } else { + return .instanceProperty + } + } + + // Functions and function-like declarations + case .keyword("func"), .keyword("init"), .keyword("deinit"), .keyword("subscript"): + if isStaticDeclaration { + return .staticMethod + } else if isClassDeclaration { + return .classMethod + } else { + return .instanceMethod + } + + default: + return nil + } + } + } + + /// Updates the given declaration tokens so it ends with at least one blank like + /// (e.g. so it ends with at least two newlines) + func endingWithBlankLine(_ tokens: [Token]) -> [Token] { + let parser = Formatter(tokens) + + // Determine how many trailing linebreaks there are in this declaration + var numberOfTrailingLinebreaks = 0 + var searchIndex = parser.tokens.count - 1 + + while searchIndex > 0, + let token = parser.token(at: searchIndex), + token.isSpaceOrCommentOrLinebreak + { + if token.isLinebreak { + numberOfTrailingLinebreaks += 1 + } + + searchIndex -= 1 + } + + // Make sure there are atleast two newlines, + // so we get a blank line between individual declaration types + while numberOfTrailingLinebreaks < 2 { + parser.insertLinebreak(at: parser.tokens.count) + numberOfTrailingLinebreaks += 1 + } + + return parser.tokens + } + + /// Organizes the flat list of declarations based on category and type + func organizeType( + _ typeDeclaration: (kind: String, open: [Token], body: [Formatter.Declaration], close: [Token])) + -> (kind: String, open: [Token], body: [Formatter.Declaration], close: [Token]) + { + // Only organize the body of classes, structs, and enums (not protocols and extensions) + guard ["class", "struct", "enum"].contains(typeDeclaration.kind) else { + return typeDeclaration + } + + // Make sure this type's body is longer than the organization threshold + let organizationThreshold: Int? + switch typeDeclaration.kind { + case "class": + organizationThreshold = formatter.options.organizeClassThreshold + case "struct": + organizationThreshold = formatter.options.organizeStructThreshold + case "enum": + organizationThreshold = formatter.options.organizeEnumThreshold + default: + organizationThreshold = nil + } + + if let organizationThreshold = organizationThreshold { + // Count the number of lines in this declaration + let lineCount = typeDeclaration.body + .flatMap { $0.tokens } + .filter { $0.isLinebreak } + .count + + // Don't organize this type's body if it is shorter than the minimum organization threshold + if lineCount < organizationThreshold { + return typeDeclaration + } + } + + var typeOpeningTokens = typeDeclaration.open + let typeClosingTokens = typeDeclaration.close + + // Categorize each of the declarations into their primary groups + let categorizedDeclarations = typeDeclaration.body.map { + (declaration: $0, + category: category(of: $0), + type: type(of: $0)) + } + + // Sort the declarations based on their category + var sortedDeclarations = categorizedDeclarations.enumerated().sorted(by: { lhs, rhs in + let (lhsOriginalIndex, lhs) = lhs + let (rhsOriginalIndex, rhs) = rhs + + // Sort primarily by category + if let lhsCategorySortOrder = categoryOrdering.index(of: lhs.category), + let rhsCategorySortOrder = categoryOrdering.index(of: rhs.category), + lhsCategorySortOrder != rhsCategorySortOrder + { + return lhsCategorySortOrder < rhsCategorySortOrder + } + + // Within individual categories (excluding .beforeMarks), sort by the declaration type + if lhs.category != .beforeMarks, + rhs.category != .beforeMarks, + let lhsType = lhs.type, + let rhsType = rhs.type, + let lhsTypeSortOrder = categorySubordering.index(of: lhsType), + let rhsTypeSortOrder = categorySubordering.index(of: rhsType), + lhsTypeSortOrder != rhsTypeSortOrder + { + return lhsTypeSortOrder < rhsTypeSortOrder + } + + // Respect the original declaration ordering when the categories and types are the same + return lhsOriginalIndex < rhsOriginalIndex + }).map { $0.element } + + // Insert comments to separate the categories + for category in categoryOrdering { + guard let indexOfFirstDeclaration = sortedDeclarations + .firstIndex(where: { $0.category == category }) + else { continue } + + // Build the MARK declaration + if let markComment = category.markComment(from: formatter.options.categoryMarkComment) { + let firstDeclaration = sortedDeclarations[indexOfFirstDeclaration].declaration + let declarationParser = Formatter(firstDeclaration.tokens) + let indentation = declarationParser.indentForLine(at: 0) + + let markDeclaration = tokenize("\(indentation)\(markComment)\n\n") + + sortedDeclarations.insert( + (.declaration(kind: "comment", tokens: markDeclaration), category, nil), + at: indexOfFirstDeclaration + ) + + // If this declaration is the first declaration in the type scope, + // make sure the type's opening sequence of tokens ends with + // at least one blank line (so the separator appears balanced) + if indexOfFirstDeclaration == 0 { + typeOpeningTokens = endingWithBlankLine(typeOpeningTokens) + } + } + + // Insert newlines to separate declaration types + for declarationType in categorySubordering { + guard let indexOfLastDeclarationWithType = sortedDeclarations + .lastIndex(where: { $0.category == category && $0.type == declarationType }) + else { continue } + + switch sortedDeclarations[indexOfLastDeclarationWithType].declaration { + case let .type(kind, open, body, close): + sortedDeclarations[indexOfLastDeclarationWithType].declaration = .type( + kind: kind, + open: open, + body: body, + close: endingWithBlankLine(close) + ) + + case let .declaration(kind, tokens): + sortedDeclarations[indexOfLastDeclarationWithType].declaration + = .declaration(kind: kind, tokens: endingWithBlankLine(tokens)) + } + } + } + + return ( + kind: typeDeclaration.kind, + open: typeOpeningTokens, + body: sortedDeclarations.map { $0.declaration }, + close: typeClosingTokens + ) + } + + /// Recursively organizes the body declarations of this declaration, + /// and any nested types. + func organize(_ declaration: Formatter.Declaration) -> Formatter.Declaration { + switch declaration { + case let .type(kind, open, body, close): + // Organize the body of this type + let (_, organizedOpen, organizedBody, organizedClose) = organizeType((kind, open, body, close)) + + // And also organize any of its nested children + return .type( + kind: kind, + open: organizedOpen, + body: organizedBody.map { organize($0) }, + close: organizedClose + ) + + // If the declaration doesn't have a body, there isn't any work to do + case .declaration: + return declaration + } + } + + // Remove all of the existing category mark comments, so they can be readded + // at the correct location after sorting the declarations. + formatter.forEach(.startOfScope("//")) { index, _ in + // Check if this comment matches an expected category separator mark comment + for category in Category.allCases { + guard let markComment = category.markComment(from: formatter.options.categoryMarkComment) else { + continue + } + + let categorySeparator = tokenize(markComment) + let potentialSeparatorRange = index ..< (index + categorySeparator.count) + + if formatter.tokens.indices.contains(potentialSeparatorRange.upperBound), + Array(formatter.tokens[potentialSeparatorRange]) == categorySeparator + { + // If we found a matching comment, remove it and all subsequent empty lines + if let nextNonwhitespaceIndex = formatter.index( + of: .nonSpaceOrLinebreak, + after: potentialSeparatorRange.upperBound + ) { + formatter.removeTokens(inRange: index ..< nextNonwhitespaceIndex) + } + } + } + } + + // Parse the file into declarations and organize the body of individual types + let organizedDeclarations = formatter + .parseDeclarations() + .map { organize($0) } + + let updatedTokens = organizedDeclarations.flatMap { $0.tokens } + + formatter.replaceTokens( + inRange: 0 ..< formatter.tokens.count, + with: updatedTokens + ) + } } diff --git a/Sources/SwiftFormat.swift b/Sources/SwiftFormat.swift index b0879a6b6..e77150c4b 100644 --- a/Sources/SwiftFormat.swift +++ b/Sources/SwiftFormat.swift @@ -520,6 +520,17 @@ private func applyRules( } } + // Rules that rewrite the entire file have to be ran first, without change tracking: + let sortedRules = rules.sorted() + let rulesThatRewriteEntireFile = sortedRules.filter { $0.rewritesEntireFile } + let standardRules = sortedRules.filter { !$0.rewritesEntireFile } + + for rule in rulesThatRewriteEntireFile { + let formatter = Formatter(tokens, options: options, trackChanges: false, range: range) + rule.apply(with: formatter) + tokens = formatter.tokens + } + // Recursively apply rules until no changes are detected let group = DispatchGroup() let queue = DispatchQueue(label: "swiftformat.formatting", qos: .userInteractive) @@ -528,7 +539,7 @@ private func applyRules( for _ in 0 ..< maxIterations { let formatter = Formatter(tokens, options: options, trackChanges: trackChanges, range: range) - for (i, rule) in rules.sorted().enumerated() { + for (i, rule) in standardRules.sorted().enumerated() { queue.async(group: group) { rule.apply(with: formatter) } diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 38bd4d59d..aa5c5b407 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -518,6 +518,40 @@ public enum Token: Equatable { return false } } + + /// Whether or not this token "defines" the specific type of declaration + /// - A valid declaration will usually include exactly one of these keywords in its outermost scope. + /// - A notable exception is `class func`, which will include two of these keywords. + public var definesDeclarationType: Bool { + // All of the keywords that map to individual Declaration grammars + // https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#grammar_declaration + let declarationKeywords = ["import", "let", "var", "typealias", "func", "enum", "case", + "struct", "class", "protocol", "init", "deinit", + "extension", "subscript", "operator", "precedencegroup"] + + return isKeyword && declarationKeywords.contains(string) + } + + /// Whether or not this token can preceed the token that `definesDeclarationType` + /// in a given declaration. e.g. `public` can preceed `var` in `public var foo = "bar"`. + public var canPrecedeDeclarationTypeKeyword: Bool { + /// All of the tokens that can typically preceed the main keyword of a declaration + if isAttribute || isKeyword || isSpaceOrCommentOrLinebreak { + return true + } + + // Some tokens are aren't treated as "keywords" by `token.isKeyword`, + // but count as keywords in the context of declarations: + let contextualKeywords = ["convenience", "dynamic", "final", "indirect", "infix", "lazy", + "mutating", "nonmutating", "open", "optional", "override", "postfix", + "precedence", "prefix", "required", "some", "unowned", "weak"] + + if isIdentifier, contextualKeywords.contains(string) { + return true + } + + return false + } } extension UnicodeScalar { diff --git a/Tests/FormatterTests.swift b/Tests/FormatterTests.swift index d08fab1b5..9db16f4f2 100644 --- a/Tests/FormatterTests.swift +++ b/Tests/FormatterTests.swift @@ -441,4 +441,260 @@ class FormatterTests: XCTestCase { """)) XCTAssertEqual(formatter.endOfScope(at: 4), 13) } + + // MARK: parseDeclarations + + func testParseDeclarations() { + let input = """ + import CoreGraphics + import Foundation + + let global = 10 + + @objc + @available(iOS 13.0, *) + @propertyWrapper("parameter") + weak var multilineGlobal = ["string"] + .map(\\.count) + let anotherGlobal = "hello" + + /// Doc comment + /// (multiple lines) + func globalFunction() { + print("hi") + } + + protocol SomeProtocol { + var getter: String { get } + func protocolMethod() -> Bool + } + + class SomeClass { + + enum NestedEnum { + /// Doc comment + case bar + func test() {} + } + + /* + * Block comment + */ + + private(set) + var instanceVar = "test" + + @objc + private var computed: String { + get { + "computed string" + } + } + + } + """ + + let originalTokens = tokenize(input) + let declarations = Formatter(originalTokens).parseDeclarations() + + // Verify we didn't lose any tokens + XCTAssertEqual(originalTokens, declarations.flatMap { $0.tokens }) + + // Verify that the tokens were grouped into the correct declarations + func string(of declaration: SwiftFormat.Formatter.Declaration?) -> String? { + guard let declaration = declaration else { return nil } + return declaration.tokens.map { $0.string }.joined() + } + + XCTAssertEqual( + string(of: declarations[0]), + """ + import CoreGraphics + + """ + ) + + XCTAssertEqual( + string(of: declarations[1]), + """ + import Foundation + + + """ + ) + + XCTAssertEqual( + string(of: declarations[2]), + """ + let global = 10 + + + """ + ) + + XCTAssertEqual( + string(of: declarations[3]), + """ + @objc + @available(iOS 13.0, *) + @propertyWrapper("parameter") + weak var multilineGlobal = ["string"] + .map(\\.count) + + """ + ) + + XCTAssertEqual( + string(of: declarations[4]), + """ + let anotherGlobal = "hello" + + + """ + ) + + XCTAssertEqual( + string(of: declarations[5]), + """ + /// Doc comment + /// (multiple lines) + func globalFunction() { + print("hi") + } + + + """ + ) + + XCTAssertEqual( + string(of: declarations[6]), + """ + protocol SomeProtocol { + var getter: String { get } + func protocolMethod() -> Bool + } + + + """ + ) + + XCTAssertEqual( + string(of: declarations[6].body?[0]), + """ + var getter: String { get } + + """ + ) + + XCTAssertEqual( + string(of: declarations[6].body?[1]), + """ + func protocolMethod() -> Bool + + """ + ) + + XCTAssertEqual( + string(of: declarations[7]), + """ + class SomeClass { + + enum NestedEnum { + /// Doc comment + case bar + func test() {} + } + + /* + * Block comment + */ + + private(set) + var instanceVar = "test" + + @objc + private var computed: String { + get { + "computed string" + } + } + + } + """ + ) + + XCTAssertEqual( + string(of: declarations[7].body?[0]), + """ + enum NestedEnum { + /// Doc comment + case bar + func test() {} + } + + + """ + ) + + XCTAssertEqual( + string(of: declarations[7].body?[0].body?[0]), + """ + /// Doc comment + case bar + + """ + ) + + XCTAssertEqual( + string(of: declarations[7].body?[0].body?[1]), + """ + func test() {} + + """ + ) + + XCTAssertEqual( + string(of: declarations[7].body?[1]), + """ + /* + * Block comment + */ + + private(set) + var instanceVar = "test" + + + """ + ) + + XCTAssertEqual( + string(of: declarations[7].body?[2]), + """ + @objc + private var computed: String { + get { + "computed string" + } + } + + + """ + ) + } + + func testParseClassFuncDeclarationCorrectly() { + // `class func` is one of the few cases (possibly only!) + // where a declaration will have more than one token that `definesDeclarationType` + let input = """ + class Foo() {} + + class func foo() {} + """ + + let originalTokens = tokenize(input) + let declarations = Formatter(originalTokens).parseDeclarations() + + XCTAssert(declarations[0].keyword == "class") + XCTAssert(declarations[1].keyword == "func") + } } diff --git a/Tests/RulesTests.swift b/Tests/RulesTests.swift index cfc5e10e5..ff810f6a1 100644 --- a/Tests/RulesTests.swift +++ b/Tests/RulesTests.swift @@ -49,7 +49,9 @@ class RulesTests: XCTestCase { precondition((0 ... 2).contains(outputs.count), "Only 0, 1 or 2 output parameters permitted") precondition(Set(exclude).intersection(rules.map { $0.name }).isEmpty, "Cannot exclude rule under test") let output = outputs.first ?? input, output2 = outputs.last ?? input - let exclude = exclude + (rules.first?.name == "linebreakAtEndOfFile" ? [] : ["linebreakAtEndOfFile"]) + let exclude = exclude + + (rules.first?.name == "linebreakAtEndOfFile" ? [] : ["linebreakAtEndOfFile"]) + + (rules.first?.name == "organizeDeclarations" ? [] : ["organizeDeclarations"]) XCTAssertEqual(try format(input, rules: rules, options: options), output) XCTAssertEqual(try format(input, rules: FormatRules.all(except: exclude), options: options), output2) @@ -13015,4 +13017,491 @@ class RulesTests: XCTestCase { let options = FormatOptions(swiftVersion: "5.2") testFormatting(for: input, rule: FormatRules.preferKeyPath, options: options) } + + // MARK: organizeDeclarations + + func testOrganizeClassDeclarationsIntoCategories() { + let input = """ + class Foo { + + private func privateMethod() {} + + private let bar = 1 + public let baz = 1 + var quux = 2 + + /* + * Block comment + */ + + init() {} + + /// Doc comment + public func publicMethod() {} + + } + """ + + let output = """ + class Foo { + + // MARK: Lifecycle + + /* + * Block comment + */ + + init() {} + + // MARK: Public + + public let baz = 1 + + /// Doc comment + public func publicMethod() {} + + // MARK: Internal + + var quux = 2 + + // MARK: Private + + private let bar = 1 + + private func privateMethod() {} + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testClassNestedInClassIsOrganized() { + let input = """ + public class Foo { + public class Bar { + fileprivate func baaz() + public var quux: Int + init() {} + deinit() {} + } + } + """ + + let output = """ + public class Foo { + + // MARK: Public + + public class Bar { + + // MARK: Lifecycle + + init() {} + deinit() {} + + // MARK: Public + + public var quux: Int + + // MARK: Fileprivate + + fileprivate func baaz() + + } + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope", "spaceAroundParens"] + ) + } + + func testStructNestedInExtensionIsOrganized() { + let input = """ + public extension Foo { + struct Bar { + private var foo: Int + private let bar: Int + + public var foobar: (Int, Int) { + (foo, bar) + } + + public init(foo: Int, bar: Int) { + self.foo = foo + self.bar = bar + } + } + } + """ + + let output = """ + public extension Foo { + struct Bar { + + // MARK: Lifecycle + + public init(foo: Int, bar: Int) { + self.foo = foo + self.bar = bar + } + + // MARK: Public + + public var foobar: (Int, Int) { + (foo, bar) + } + + // MARK: Private + + private var foo: Int + private let bar: Int + + } + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testOrganizePrivateSet() { + let input = """ + struct Foo { + public private(set) var bar: Int + private(set) var baz: Int + internal private(set) var baz: Int + } + """ + + let output = """ + struct Foo { + + // MARK: Public + + public private(set) var bar: Int + + // MARK: Internal + + private(set) var baz: Int + internal private(set) var baz: Int + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testSortDeclarationTypes() { + let input = """ + struct Foo { + static var a1: Int = 1 + static var a2: Int = 2 + var d: CGFloat { + 3.141592653589 + } + + func g() -> Int { + 10 + } + + let c: String = String { + "closure body" + }() + + static func e() {} + + static var b: String { + "computed property" + } + + class func f() -> Foo { + Foo() + } + + enum NestedEnum {} + } + """ + + let output = """ + struct Foo { + + // MARK: Internal + + enum NestedEnum {} + + static var a1: Int = 1 + static var a2: Int = 2 + + static var b: String { + "computed property" + } + + let c: String = String { + "closure body" + }() + + var d: CGFloat { + 3.141592653589 + } + + static func e() {} + + class func f() -> Foo { + Foo() + } + + func g() -> Int { + 10 + } + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testOrganizeEnumCasesFirst() { + let input = """ + enum Foo { + init?(rawValue: String) { + return nil + } + + case bar + case baz + case quux + } + """ + + let output = """ + enum Foo { + case bar + case baz + case quux + + // MARK: Lifecycle + + init?(rawValue: String) { + return nil + } + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope", "unusedArguments"] + ) + } + + func testPlacingCustomDeclarationsBeforeMarks() { + let input = """ + struct Foo { + + public init() {} + + public typealias Bar = Int + + public struct Baz {} + + } + """ + + let output = """ + struct Foo { + + public typealias Bar = Int + + public struct Baz {} + + // MARK: Lifecycle + + public init() {} + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + options: FormatOptions(beforeMarks: ["typealias", "struct"]), + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testCustomLifecycleMethods() { + let input = """ + class ViewController: UIViewController { + + public init() { + super.init(nibName: nil, bundle: nil) + } + + func viewDidLoad() { + super.viewDidLoad() + } + + func internalInstanceMethod() {} + + func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + } + """ + + let output = """ + class ViewController: UIViewController { + + // MARK: Lifecycle + + public init() { + super.init(nibName: nil, bundle: nil) + } + + func viewDidLoad() { + super.viewDidLoad() + } + + func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + // MARK: Internal + + func internalInstanceMethod() {} + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + options: FormatOptions(lifecycleMethods: ["viewDidLoad", "viewWillAppear", "viewDidAppear"]), + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testCustomCategoryMarkTemplate() { + let input = """ + struct Foo { + public init() {} + public func publicInstanceMethod() {} + } + """ + + let output = """ + struct Foo { + + // - Lifecycle + + public init() {} + + // - Public + + public func publicInstanceMethod() {} + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + options: FormatOptions(categoryMarkComment: "- %c"), + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testBelowCustomStructOrganizationThreshold() { + let input = """ + struct StructBelowThreshold { + init() {} + } + """ + + testFormatting( + for: input, + rule: FormatRules.organizeDeclarations, + options: FormatOptions(organizeStructThreshold: 2) + ) + } + + func testAboveCustomStructOrganizationThreshold() { + let input = """ + struct StructAboveThreshold { + init() {} + public func instanceMethod() {} + } + """ + + let output = """ + struct StructAboveThreshold { + + // MARK: Lifecycle + + init() {} + + // MARK: Public + + public func instanceMethod() {} + + } + """ + + testFormatting( + for: input, output, + rule: FormatRules.organizeDeclarations, + options: FormatOptions(organizeStructThreshold: 2), + exclude: ["blankLinesAtStartOfScope", "blankLinesAtEndOfScope"] + ) + } + + func testCustomClassOrganizationThreshold() { + let input = """ + class ClassBelowThreshold { + init() {} + } + """ + + testFormatting( + for: input, + rule: FormatRules.organizeDeclarations, + options: FormatOptions(organizeClassThreshold: 2) + ) + } + + func testCustomEnumOrganizationThreshold() { + let input = """ + enum EnumBelowThreshold { + case enumCase + } + """ + + testFormatting( + for: input, + rule: FormatRules.organizeDeclarations, + options: FormatOptions(organizeEnumThreshold: 2) + ) + } } diff --git a/Tests/XCTestManifests.swift b/Tests/XCTestManifests.swift index 406e26f91..459cb6110 100644 --- a/Tests/XCTestManifests.swift +++ b/Tests/XCTestManifests.swift @@ -150,6 +150,8 @@ extension FormatterTests { ("testMalformedDirective", testMalformedDirective), ("testMalformedOption", testMalformedOption), ("testOriginalLinePreservedAfterFormatting", testOriginalLinePreservedAfterFormatting), + ("testParseClassFuncDeclarationCorrectly", testParseClassFuncDeclarationCorrectly), + ("testParseDeclarations", testParseDeclarations), ("testRemoveCurrentTokenWhileEnumerating", testRemoveCurrentTokenWhileEnumerating), ("testRemoveNextTokenWhileEnumerating", testRemoveNextTokenWhileEnumerating), ("testRemovePreviousTokenWhileEnumerating", testRemovePreviousTokenWhileEnumerating), @@ -411,6 +413,7 @@ extension RulesTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__RulesTests = [ + ("testAboveCustomStructOrganizationThreshold", testAboveCustomStructOrganizationThreshold), ("testAddSpaceAfterFuncEquals", testAddSpaceAfterFuncEquals), ("testAddSpaceAfterOperatorEquals", testAddSpaceAfterOperatorEquals), ("testAddSpaceAroundRange", testAddSpaceAroundRange), @@ -471,6 +474,7 @@ extension RulesTests { ("testBeforeFirstPreservedAndTrailingCommaAddedInSingleLineNestedDictionaryWithOneNestedItem", testBeforeFirstPreservedAndTrailingCommaAddedInSingleLineNestedDictionaryWithOneNestedItem), ("testBeforeFirstPreservedIndentFixed", testBeforeFirstPreservedIndentFixed), ("testBeforeFirstPreservedNewlineAdded", testBeforeFirstPreservedNewlineAdded), + ("testBelowCustomStructOrganizationThreshold", testBelowCustomStructOrganizationThreshold), ("testBinaryGroupingCustom", testBinaryGroupingCustom), ("testBlankCodeCommentBlockLinesNotIndented", testBlankCodeCommentBlockLinesNotIndented), ("testBlankLineAfterProtocolBeforeProperty", testBlankLineAfterProtocolBeforeProperty), @@ -529,6 +533,7 @@ extension RulesTests { ("testClassFuncAttributeTreatedAsFunction", testClassFuncAttributeTreatedAsFunction), ("testClassImportAttributeNotTreatedAsType", testClassImportAttributeNotTreatedAsType), ("testClassImportNotReplacedByAnyObject", testClassImportNotReplacedByAnyObject), + ("testClassNestedInClassIsOrganized", testClassNestedInClassIsOrganized), ("testClassNotReplacedByAnyObjectIfSwiftVersionLessThan4_1", testClassNotReplacedByAnyObjectIfSwiftVersionLessThan4_1), ("testClassReplacedByAnyObject", testClassReplacedByAnyObject), ("testClassReplacedByAnyObjectImmediatelyAfterImport", testClassReplacedByAnyObjectImmediatelyAfterImport), @@ -601,7 +606,11 @@ extension RulesTests { ("testCountNotEqualToZero", testCountNotEqualToZero), ("testCurriedFunctionCallNotUnwrapped", testCurriedFunctionCallNotUnwrapped), ("testCurriedFunctionCallNotUnwrapped2", testCurriedFunctionCallNotUnwrapped2), + ("testCustomCategoryMarkTemplate", testCustomCategoryMarkTemplate), + ("testCustomClassOrganizationThreshold", testCustomClassOrganizationThreshold), + ("testCustomEnumOrganizationThreshold", testCustomEnumOrganizationThreshold), ("testCustomHexGrouping", testCustomHexGrouping), + ("testCustomLifecycleMethods", testCustomLifecycleMethods), ("testCustomMethodMadeTrailing", testCustomMethodMadeTrailing), ("testCustomOctalGrouping", testCustomOctalGrouping), ("testDecimalGroupingThousands", testDecimalGroupingThousands), @@ -1305,6 +1314,9 @@ extension RulesTests { ("testOptionalCountNotEqualToZero", testOptionalCountNotEqualToZero), ("testOptionalFunctionCallNotUnwrapped", testOptionalFunctionCallNotUnwrapped), ("testOptionalTypeConvertedToSugar", testOptionalTypeConvertedToSugar), + ("testOrganizeClassDeclarationsIntoCategories", testOrganizeClassDeclarationsIntoCategories), + ("testOrganizeEnumCasesFirst", testOrganizeEnumCasesFirst), + ("testOrganizePrivateSet", testOrganizePrivateSet), ("testOuterParensRemovedInIf", testOuterParensRemovedInIf), ("testOuterParensRemovedInWhile", testOuterParensRemovedInWhile), ("testOverriddenFileprivateInitNotChangedToPrivate", testOverriddenFileprivateInitNotChangedToPrivate), @@ -1376,6 +1388,7 @@ extension RulesTests { ("testPerformBatchUpdatesNotMadeTrailing", testPerformBatchUpdatesNotMadeTrailing), ("testPInExponentialNotConvertedToLower", testPInExponentialNotConvertedToLower), ("testPInExponentialNotConvertedToUpper", testPInExponentialNotConvertedToUpper), + ("testPlacingCustomDeclarationsBeforeMarks", testPlacingCustomDeclarationsBeforeMarks), ("testPostfixExpressionNonYodaCondition", testPostfixExpressionNonYodaCondition), ("testPostfixExpressionNonYodaCondition2", testPostfixExpressionNonYodaCondition2), ("testPostfixExpressionYodaCondition", testPostfixExpressionYodaCondition), @@ -1605,6 +1618,7 @@ extension RulesTests { ("testSolitaryClosureMadeTrailingForNumericTupleMember", testSolitaryClosureMadeTrailingForNumericTupleMember), ("testSolitaryClosureMadeTrailingInChain", testSolitaryClosureMadeTrailingInChain), ("testSortContiguousImports", testSortContiguousImports), + ("testSortDeclarationTypes", testSortDeclarationTypes), ("testSortedImportEnum", testSortedImportEnum), ("testSortedImportFunc", testSortedImportFunc), ("testSortedImportsDoesntMoveHeaderComment", testSortedImportsDoesntMoveHeaderComment), @@ -1722,6 +1736,7 @@ extension RulesTests { ("testStripHeader", testStripHeader), ("testStrippingSwiftNamespaceDoesNotStripPreviousSwiftNamespaceReferences", testStrippingSwiftNamespaceDoesNotStripPreviousSwiftNamespaceReferences), ("testStrippingSwiftNamespaceInOptionalTypeWhenConvertedToSugar", testStrippingSwiftNamespaceInOptionalTypeWhenConvertedToSugar), + ("testStructNestedInExtensionIsOrganized", testStructNestedInExtensionIsOrganized), ("testsTupleNotUnwrapped", testsTupleNotUnwrapped), ("testsTupleOfClosuresNotUnwrapped", testsTupleOfClosuresNotUnwrapped), ("testSubscriptFunctionCallNotUnwrapped", testSubscriptFunctionCallNotUnwrapped),