// // SortDeclarations.swift // SwiftFormat // // Created by Cal Stephens on 11/22/21. // Copyright © 2024 Nick Lockwood. All rights reserved. // import Foundation public extension FormatRule { static let sortDeclarations = FormatRule( help: """ Sorts the body of declarations with // swiftformat:sort and declarations between // swiftformat:sort:begin and // swiftformat:sort:end comments. """, options: ["sorted-patterns"], sharedOptions: ["linebreaks", "organize-types", "struct-threshold", "class-threshold", "enum-threshold", "extension-threshold"] ) { formatter in formatter.forEachToken( where: { $0.isCommentBody && $0.string.contains("swiftformat:sort") || $0.isDeclarationTypeKeyword(including: Array(Token.swiftTypeKeywords)) } ) { index, token in let rangeToSort: ClosedRange let numberOfLeadingLinebreaks: Int // For `:sort:begin`, directives, we sort the declarations // between the `:begin` and and `:end` comments let shouldBePartiallySorted = token.string.contains("swiftformat:sort:begin") let identifier = formatter.next(.identifier, after: index) let shouldBeSortedByNamePattern = formatter.options.alphabeticallySortedDeclarationPatterns.contains { identifier?.string.contains($0) ?? false } let shouldBeSortedByMarkComment = token.isCommentBody && !token.string.contains(":sort:") // For `:sort` directives and types with matching name pattern, we sort the declarations // between the open and close brace of the following type let shouldBeFullySorted = shouldBeSortedByNamePattern || shouldBeSortedByMarkComment if shouldBePartiallySorted { guard let endCommentIndex = formatter.tokens[index...].firstIndex(where: { $0.isComment && $0.string.contains("swiftformat:sort:end") }), let sortRangeStart = formatter.index(of: .nonSpaceOrComment, after: index), let firstRangeToken = formatter.index(of: .nonLinebreak, after: sortRangeStart), let lastRangeToken = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: endCommentIndex - 2) else { return } rangeToSort = sortRangeStart ... lastRangeToken numberOfLeadingLinebreaks = firstRangeToken - sortRangeStart } else if shouldBeFullySorted { guard let typeOpenBrace = formatter.index(of: .startOfScope("{"), after: index), let typeCloseBrace = formatter.endOfScope(at: typeOpenBrace), let firstTypeBodyToken = formatter.index(of: .nonLinebreak, after: typeOpenBrace), let lastTypeBodyToken = formatter.index(of: .nonLinebreak, before: typeCloseBrace), let declarationKeywordIndex = formatter.indexOfLastSignificantKeyword(at: typeOpenBrace), lastTypeBodyToken > typeOpenBrace else { return } // Sorting the body of a type conflicts with the `organizeDeclarations` // keyword if enabled for this declaration. In that case, // defer to the sorting implementation in `organizeDeclarations`. if formatter.options.enabledRules.contains(FormatRule.organizeDeclarations.name), formatter.options.organizeTypes.contains(formatter.tokens[declarationKeywordIndex].string), formatter.typeLengthExceedsOrganizationThreshold(at: declarationKeywordIndex) { return } rangeToSort = firstTypeBodyToken ... lastTypeBodyToken // We don't include any leading linebreaks in the range to sort, // since `firstTypeBodyToken` is the first `nonLinebreak` in the body numberOfLeadingLinebreaks = 0 } else { return } var declarations = Formatter(Array(formatter.tokens[rangeToSort])) .parseDeclarations() .enumerated() .sorted(by: { lhs, rhs -> Bool in let (lhsIndex, lhsDeclaration) = lhs let (rhsIndex, rhsDeclaration) = rhs // Primarily sort by name, to alphabetize if let lhsName = lhsDeclaration.name, let rhsName = rhsDeclaration.name, lhsName != rhsName { return lhsName.localizedCompare(rhsName) == .orderedAscending } // Otherwise preserve the existing order else { return lhsIndex < rhsIndex } }) .map(\.element) // Make sure there's at least one newline between each declaration for i in 0 ..< max(0, declarations.count - 1) { let declaration = declarations[i] let nextDeclaration = declarations[i + 1] if declaration.tokens.last?.isLinebreak == false, nextDeclaration.tokens.first?.isLinebreak == false { let declarationNeedingLinebreak = declarations[i + 1] declarationNeedingLinebreak.formatter.insertLinebreak(at: declarationNeedingLinebreak.range.lowerBound) } } var sortedFormatter = Formatter(declarations.flatMap(\.tokens)) // Make sure the type has the same number of leading line breaks // as it did before sorting if let currentLeadingLinebreakCount = sortedFormatter.tokens.firstIndex(where: { !$0.isLinebreak }) { if numberOfLeadingLinebreaks != currentLeadingLinebreakCount { sortedFormatter.removeTokens(in: 0 ..< currentLeadingLinebreakCount) for _ in 0 ..< numberOfLeadingLinebreaks { sortedFormatter.insertLinebreak(at: 0) } } } else { for _ in 0 ..< numberOfLeadingLinebreaks { sortedFormatter.insertLinebreak(at: 0) } } // There are always expected to be zero trailing line breaks, // so we remove any trailing line breaks // (this is because `typeBodyRange` specifically ends before the first // trailing linebreak) while sortedFormatter.tokens.last?.isLinebreak == true { sortedFormatter.removeLastToken() } if Array(formatter.tokens[rangeToSort]) != sortedFormatter.tokens { formatter.replaceTokens( in: rangeToSort, with: sortedFormatter.tokens ) } } } examples: { """ ```diff // swiftformat:sort enum FeatureFlags { - case upsellB - case fooFeature - case barFeature - case upsellA( - fooConfiguration: Foo, - barConfiguration: Bar) + case barFeature + case fooFeature + case upsellA( + fooConfiguration: Foo, + barConfiguration: Bar) + case upsellB } /// With --sortedpatterns Feature enum FeatureFlags { - case upsellB - case fooFeature - case barFeature - case upsellA( - fooConfiguration: Foo, - barConfiguration: Bar) + case barFeature + case fooFeature + case upsellA( + fooConfiguration: Foo, + barConfiguration: Bar) + case upsellB } enum FeatureFlags { // swiftformat:sort:begin - case upsellB - case fooFeature - case barFeature - case upsellA( - fooConfiguration: Foo, - barConfiguration: Bar) + case barFeature + case fooFeature + case upsellA( + fooConfiguration: Foo, + barConfiguration: Bar) + case upsellB // swiftformat:sort:end var anUnsortedProperty: Foo { Foo() } } ``` """ } }