mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
835f6f287d
Co-authored-by: calda <1811727+calda@users.noreply.github.com>
995 lines
42 KiB
Swift
995 lines
42 KiB
Swift
//
|
|
// OrganizeDeclarations.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Cal Stephens on 8/16/20.
|
|
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
static let organizeDeclarations = FormatRule(
|
|
help: "Organize declarations within class, struct, enum, actor, and extension bodies.",
|
|
runOnceOnly: true,
|
|
disabledByDefault: true,
|
|
orderAfter: [.extensionAccessControl, .redundantFileprivate, .redundantPublic, .validateTestCases, .redundantMemberwiseInit],
|
|
options: [
|
|
"category-mark", "mark-categories", "before-marks",
|
|
"lifecycle", "organize-types", "struct-threshold", "class-threshold",
|
|
"enum-threshold", "extension-threshold", "mark-struct-threshold",
|
|
"mark-class-threshold", "mark-enum-threshold", "mark-extension-threshold",
|
|
"organization-mode", "type-body-marks", "visibility-order", "type-order", "visibility-marks",
|
|
"type-marks", "group-blank-lines", "sort-swiftui-properties",
|
|
],
|
|
sharedOptions: ["sorted-patterns", "line-after-marks", "linebreaks"]
|
|
) { formatter in
|
|
formatter.parseDeclarations().forEachRecursiveDeclaration { declaration in
|
|
// Organize the body of type declarations
|
|
guard let typeDeclaration = declaration.asTypeDeclaration else { return }
|
|
formatter.organizeDeclaration(typeDeclaration)
|
|
}
|
|
} examples: {
|
|
"""
|
|
Default value for `--visibility-order` when using `--organization-mode visibility`:
|
|
`\(VisibilityCategory.defaultOrdering(for: .visibility).map(\.rawValue).joined(separator: ", "))`
|
|
|
|
Default value for `--visibility-order` when using `--organization-mode type`:
|
|
`\(VisibilityCategory.defaultOrdering(for: .type).map(\.rawValue).joined(separator: ", "))`
|
|
|
|
**NOTE:** When providing custom arguments for `--visibility-order` the following entries must be included:
|
|
`\(VisibilityCategory.essentialCases.map(\.rawValue).joined(separator: ", "))`
|
|
|
|
Default value for `--type-order` when using `--organization-mode visibility`:
|
|
`\(DeclarationType.defaultOrdering(for: .visibility).map(\.rawValue).joined(separator: ", "))`
|
|
|
|
Default value for `--type-order` when using `--organization-mode type`:
|
|
`\(DeclarationType.defaultOrdering(for: .type).map(\.rawValue).joined(separator: ", "))`
|
|
|
|
**NOTE:** The follow declaration types must be included in either `--type-order` or `--visibility-order`:
|
|
`\(DeclarationType.essentialCases.map(\.rawValue).joined(separator: ", "))`
|
|
|
|
**NOTE:** The Swift compiler automatically synthesizes a memberwise `init` for `struct` types.
|
|
|
|
To allow SwiftFormat to reorganize your code effectively, you must explicitly declare an `init`.
|
|
Without this declaration, only functions will be reordered, while properties will remain in their original order.
|
|
|
|
`--organization-mode visibility` (default)
|
|
|
|
```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
|
|
+
|
|
}
|
|
```
|
|
|
|
`--organization-mode type`
|
|
|
|
```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: Properties
|
|
+
|
|
+ public let a: Int = 1
|
|
+ public let b: Int = 3
|
|
+
|
|
+ let e: Int = 2
|
|
+
|
|
+ private let g: Int = 2
|
|
+
|
|
+ // MARK: Lifecycle
|
|
+
|
|
+ init() {}
|
|
+ deinit() {}
|
|
+
|
|
+ // MARK: Functions
|
|
+
|
|
+ public func c() -> String {}
|
|
+ public func d() {}
|
|
+
|
|
}
|
|
```
|
|
"""
|
|
}
|
|
}
|
|
|
|
// MARK: - organizeDeclaration
|
|
|
|
extension Formatter {
|
|
/// Organizes the given type declaration into sorted categories
|
|
func organizeDeclaration(_ typeDeclaration: TypeDeclaration) {
|
|
guard !typeDeclaration.body.isEmpty,
|
|
options.organizeTypes.contains(typeDeclaration.keyword),
|
|
typeLengthExceedsOrganizationThreshold(at: typeDeclaration.keywordIndex)
|
|
else { return }
|
|
|
|
// Parse category order from options
|
|
let categoryOrder = categoryOrder(for: options.organizationMode)
|
|
|
|
// Adjust the ranges of the type's body declarations so that any
|
|
// existing MARK comment is the first tokens in any declaration.
|
|
adjustBodyDeclarationRanges(in: typeDeclaration, order: categoryOrder)
|
|
|
|
// Track the consecutive groups of property declarations so we can avoid inserting
|
|
// blank lines between elements in the group if possible.
|
|
let consecutivePropertyGroups = consecutivePropertyDeclarationGroups(in: typeDeclaration)
|
|
.filter { group in
|
|
// Only track declaration groups where the group as a whole is followed by a
|
|
// blank line, since otherwise the declarations can be reordered without issues.
|
|
guard let lastDeclarationInGroup = group.last else { return false }
|
|
return lastDeclarationInGroup.tokens.numberOfTrailingLinebreaks() > 1
|
|
}
|
|
|
|
// Categorize each of the declarations into their primary groups
|
|
let categorizedDeclarations = typeDeclaration.body.map { declaration in
|
|
(declaration: declaration, category: category(of: declaration, using: categoryOrder))
|
|
}
|
|
|
|
// Sort the declarations based on their category and type
|
|
guard let sortedTypeBody = sortCategorizedDeclarations(
|
|
categorizedDeclarations,
|
|
in: typeDeclaration
|
|
) else { return }
|
|
|
|
typeDeclaration.updateBody(to: sortedTypeBody.map(\.declaration))
|
|
|
|
// Add a mark comment for each top-level category
|
|
addCategorySeparators(to: sortedTypeBody, in: typeDeclaration, order: categoryOrder)
|
|
|
|
// Preserve the expected spacing for any groups of properties that were
|
|
// conseutive in the original declaration ordering.
|
|
preserveConsecutivePropertyGroupSpacing(
|
|
in: typeDeclaration,
|
|
groups: consecutivePropertyGroups,
|
|
order: categoryOrder
|
|
)
|
|
}
|
|
|
|
typealias CategorizedDeclaration = (declaration: Declaration, category: Category)
|
|
|
|
/// Sorts the given categorized declarations based on the defined category ordering
|
|
func sortCategorizedDeclarations(
|
|
_ categorizedDeclarations: [CategorizedDeclaration],
|
|
in typeDeclaration: TypeDeclaration
|
|
) -> [CategorizedDeclaration]? {
|
|
let sortAlphabeticallyWithinSubcategories = shouldSortAlphabeticallyWithinSubcategories(in: typeDeclaration)
|
|
|
|
var sortedDeclarations = sortDeclarations(
|
|
categorizedDeclarations,
|
|
sortAlphabeticallyWithinSubcategories: sortAlphabeticallyWithinSubcategories
|
|
)
|
|
|
|
// The compiler will synthesize a memberwise init for `struct` declarations that don't have an `init` declaration.
|
|
// We have to ensure we preserve the relative order of declarations that appear in the synthesized init.
|
|
if typeDeclaration.keyword == "struct",
|
|
!sortAlphabeticallyWithinSubcategories,
|
|
!typeDeclaration.body.contains(where: { $0.keyword == "init" })
|
|
{
|
|
let requiredSubordering = categorizedDeclarations.filter { affectsSynthesizedMemberwiseInitializerParameterOrdering($0.declaration) }
|
|
|
|
if !requiredSubordering.isEmpty {
|
|
for index in requiredSubordering.indices.dropFirst() {
|
|
let declarationToReorder = requiredSubordering[index]
|
|
let currentIndex = sortedDeclarations.firstIndex(where: { $0.declaration === declarationToReorder.declaration })!
|
|
let requiredPreviousDeclaration = requiredSubordering[index - 1]
|
|
let currentIndexOfPreviousDeclaration = sortedDeclarations.firstIndex(where: { $0.declaration === requiredPreviousDeclaration.declaration })!
|
|
|
|
// If this declaration is ordered before the next required declaration, move it to be after it. This preserves the required ordering.
|
|
if currentIndex < currentIndexOfPreviousDeclaration {
|
|
sortedDeclarations.insert(declarationToReorder, at: currentIndexOfPreviousDeclaration + 1)
|
|
sortedDeclarations.remove(at: currentIndex)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return sortedDeclarations
|
|
}
|
|
|
|
func sortDeclarations(
|
|
_ categorizedDeclarations: [CategorizedDeclaration],
|
|
sortAlphabeticallyWithinSubcategories: Bool
|
|
) -> [CategorizedDeclaration] {
|
|
let customDeclarationSortOrder = customDeclarationSortOrderList(from: categorizedDeclarations)
|
|
return categorizedDeclarations.enumerated()
|
|
.sorted(by: { lhs, rhs in
|
|
let (lhsOriginalIndex, lhs) = lhs
|
|
let (rhsOriginalIndex, rhs) = rhs
|
|
|
|
if lhs.category.order != rhs.category.order {
|
|
return lhs.category.order < rhs.category.order
|
|
}
|
|
|
|
// If this type had a :sort directive, we sort alphabetically
|
|
// within the subcategories (where ordering is otherwise undefined)
|
|
if sortAlphabeticallyWithinSubcategories,
|
|
let lhsName = lhs.declaration.name,
|
|
let rhsName = rhs.declaration.name,
|
|
lhsName != rhsName
|
|
{
|
|
return lhsName.localizedCompare(rhsName) == .orderedAscending
|
|
}
|
|
|
|
if lhs.category.type == rhs.category.type,
|
|
let lhs = lhs.declaration.swiftUIPropertyWrapper,
|
|
let rhs = rhs.declaration.swiftUIPropertyWrapper
|
|
{
|
|
switch options.swiftUIPropertiesSortMode {
|
|
case .none:
|
|
break
|
|
case .alphabetize:
|
|
return lhs.localizedCompare(rhs) == .orderedAscending
|
|
case .firstAppearanceSort:
|
|
return customDeclarationSortOrder.areInRelativeOrder(lhs: lhs, rhs: rhs)
|
|
}
|
|
}
|
|
|
|
// Respect the original declaration ordering when the categories and types are the same
|
|
return lhsOriginalIndex < rhsOriginalIndex
|
|
})
|
|
.map(\.element)
|
|
}
|
|
|
|
func customDeclarationSortOrderList(from categorizedDeclarations: [CategorizedDeclaration]) -> [String] {
|
|
guard options.swiftUIPropertiesSortMode == .firstAppearanceSort else { return [] }
|
|
return categorizedDeclarations
|
|
.compactMap(\.declaration.swiftUIPropertyWrapper)
|
|
.firstElementAppearanceOrder()
|
|
}
|
|
|
|
/// Whether or not type members should additionally be sorted alphabetically
|
|
/// within individual subcategories
|
|
func shouldSortAlphabeticallyWithinSubcategories(in typeDeclaration: TypeDeclaration) -> Bool {
|
|
let rangeBeforeKeyword = typeDeclaration.range.lowerBound ..< typeDeclaration.keywordIndex
|
|
// If this type has a leading :sort directive, we sort alphabetically
|
|
// within the subcategories (where ordering is otherwise undefined)
|
|
let shouldSortAlphabeticallyBySortingMark = tokens[rangeBeforeKeyword].contains(where: {
|
|
$0.isCommentBody && $0.string.contains("swiftformat:sort") && !$0.string.contains(":sort:")
|
|
})
|
|
|
|
// If this type declaration name contains pattern — sort as well
|
|
let shouldSortAlphabeticallyByDeclarationPattern: Bool = {
|
|
guard let name = typeDeclaration.name else { return false }
|
|
|
|
return options.alphabeticallySortedDeclarationPatterns.contains {
|
|
name.contains($0)
|
|
}
|
|
}()
|
|
|
|
return shouldSortAlphabeticallyBySortingMark
|
|
|| shouldSortAlphabeticallyByDeclarationPattern
|
|
}
|
|
|
|
/// Whether or not this declaration is an instance property that can affect
|
|
/// the the ordering of parameters in the struct's synthesized memberwise initializer
|
|
func affectsSynthesizedMemberwiseInitializerParameterOrdering(_ declaration: Declaration) -> Bool {
|
|
guard declaration.isStoredInstanceProperty else { return false }
|
|
|
|
lazy var hasDefaultValue = {
|
|
// The SwiftUI `@Environment` modifier always provides a default value
|
|
if declaration.hasModifier("@Environment") {
|
|
return true
|
|
}
|
|
|
|
guard let property = declaration.parsePropertyDeclaration() else {
|
|
return false
|
|
}
|
|
|
|
if property.value != nil {
|
|
return true
|
|
}
|
|
|
|
// Optional variables default to `nil`
|
|
if declaration.keyword == "var", property.type?.isOptionalType == true {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}()
|
|
|
|
// `let` properties with default values are not part of the memberwise init.
|
|
// `var` properties with default values ARE part of it (as optional params).
|
|
if declaration.keyword == "let", hasDefaultValue {
|
|
return false
|
|
}
|
|
|
|
// Private property wrappers with a default value are excluded from the memberwise initializer
|
|
if declaration.swiftUIPropertyWrapper != nil,
|
|
[.private, .fileprivate].contains(declaration.visibility()),
|
|
hasDefaultValue
|
|
{
|
|
return false
|
|
}
|
|
|
|
// Assumption: in practice, any private property would only affect a private memberwise init,
|
|
// which is not very common or useful.
|
|
if declaration.visibility() == .private {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/// Whether or not the two given declaration orderings preserve
|
|
/// the same synthesized memberwise initializer
|
|
func preservesSynthesizedMemberwiseInitializer(
|
|
_ lhs: [CategorizedDeclaration],
|
|
_ rhs: [CategorizedDeclaration]
|
|
) -> Bool {
|
|
let lhsPropertiesOrder = lhs
|
|
.filter { affectsSynthesizedMemberwiseInitializerParameterOrdering($0.declaration) }
|
|
.map(\.declaration)
|
|
|
|
let rhsPropertiesOrder = rhs
|
|
.filter { affectsSynthesizedMemberwiseInitializerParameterOrdering($0.declaration) }
|
|
.map(\.declaration)
|
|
|
|
return lhsPropertiesOrder.elementsEqual(rhsPropertiesOrder, by: { lhs, rhs in
|
|
lhs === rhs
|
|
})
|
|
}
|
|
|
|
/// Adjust the ranges of the type's body declarations so that any existing MARK comment
|
|
/// is the first token in any declaration. This makes it so that any comment _before_
|
|
/// the MARK comment is treated as part of the previous declaration.
|
|
func adjustBodyDeclarationRanges(in typeDeclaration: TypeDeclaration, order: ParsedOrder) {
|
|
for (index, declaration) in typeDeclaration.body.enumerated() {
|
|
guard index != 0 else { continue }
|
|
|
|
let matchingComments = matchingCategorySeparatorComments(in: declaration.leadingCommentRange, order: order)
|
|
guard let markCommentRange = matchingComments.first,
|
|
let newlineBeforeMarkComment = self.index(of: .linebreak, before: markCommentRange.lowerBound)
|
|
else { continue }
|
|
|
|
let previousDeclaration = typeDeclaration.body[index - 1]
|
|
|
|
previousDeclaration.range = previousDeclaration.range.lowerBound ... newlineBeforeMarkComment
|
|
declaration.range = (newlineBeforeMarkComment + 1) ... declaration.range.upperBound
|
|
}
|
|
}
|
|
|
|
/// Adds MARK category separates to the given type
|
|
func addCategorySeparators(
|
|
to sortedDeclarations: [CategorizedDeclaration],
|
|
in typeDeclaration: TypeDeclaration,
|
|
order: ParsedOrder
|
|
) {
|
|
let typeExceedsThresholdToAddMarks = typeLengthExceedsMarkThreshold(at: typeDeclaration.keywordIndex)
|
|
|
|
let numberOfCategories: Int = {
|
|
switch options.organizationMode {
|
|
case .visibility:
|
|
if typeDeclaration.keyword == "protocol" {
|
|
// There is no access control in protocols, so all declarations are part of the same category.
|
|
return 1
|
|
} else {
|
|
return Set(sortedDeclarations.map(\.category).map(\.visibility)).count
|
|
}
|
|
|
|
case .type:
|
|
return Set(sortedDeclarations.map(\.category).map(\.type)).count
|
|
}
|
|
}()
|
|
|
|
var formattedCategories: [Category] = []
|
|
|
|
for (index, (declaration, category)) in sortedDeclarations.enumerated() {
|
|
if options.markCategories,
|
|
typeExceedsThresholdToAddMarks,
|
|
numberOfCategories > 1,
|
|
let markCommentBody = category.markCommentBody(from: options.categoryMarkComment, with: options.organizationMode),
|
|
category.shouldBeMarked(in: Set(formattedCategories), for: options.organizationMode)
|
|
{
|
|
formattedCategories.append(category)
|
|
|
|
let indentation = currentIndentForLine(at: declaration.range.lowerBound)
|
|
let markDeclaration = tokenize("\(indentation)// \(markCommentBody)")
|
|
let eligibleCommentRange = declaration.range.lowerBound ..< self.index(of: .nonSpaceOrCommentOrLinebreak, after: declaration.range.lowerBound - 1)!
|
|
|
|
// Remove any comments other than the expected mark comment if present
|
|
removeExistingCategorySeparators(
|
|
from: declaration,
|
|
previousDeclaration: index == 0 ? nil : sortedDeclarations[index - 1].declaration,
|
|
order: order,
|
|
preserving: { commentBody in
|
|
commentBody == markCommentBody
|
|
}
|
|
)
|
|
|
|
let matchingComments = singleLineComments(in: eligibleCommentRange, matching: { commentBody in
|
|
commentBody == markCommentBody
|
|
})
|
|
|
|
if matchingComments.count == 1, let matchingComment = matchingComments.first {
|
|
// The declaration already has the expected mark comment.
|
|
// However, we need to make sure it also has a trailing blank line.
|
|
if options.lineAfterMarks,
|
|
let tokenAfterComment = self.index(of: .nonSpaceOrComment, after: matchingComment.upperBound),
|
|
tokens[tokenAfterComment].isLinebreak,
|
|
let nextToken = self.index(of: .nonSpaceOrComment, after: tokenAfterComment),
|
|
!tokens[nextToken].isLinebreak
|
|
{
|
|
insertLinebreak(at: tokenAfterComment)
|
|
}
|
|
} else {
|
|
insertLinebreak(at: declaration.range.lowerBound)
|
|
if options.lineAfterMarks {
|
|
insertLinebreak(at: declaration.range.lowerBound)
|
|
}
|
|
|
|
insert(markDeclaration, at: declaration.range.lowerBound)
|
|
}
|
|
|
|
// If this declaration is the first declaration in the type scope,
|
|
// make sure the type's body starts with at least one blank line
|
|
// so the category separator appears balanced
|
|
if index == 0 {
|
|
let tokensBetweenStartOfScopeAndFirstDeclaration =
|
|
tokens[typeDeclaration.openBraceIndex ..< typeDeclaration.body[0].range.lowerBound]
|
|
|
|
// Compute how many linebreaks are needed up-front rather than using a
|
|
// `while` loop, to avoid an infinite loop when there is content (e.g. a
|
|
// trailing comment) on the same line as the opening brace. In that case
|
|
// `body[0].range.lowerBound == openBraceIndex + 1`, so inserting at
|
|
// `openBraceIndex + 1` never shifts `body[0].range.lowerBound` and the
|
|
// while-loop condition would never become false.
|
|
let neededLinebreaks = max(
|
|
0, 2 - tokensBetweenStartOfScopeAndFirstDeclaration.numberOfTrailingLinebreaks()
|
|
)
|
|
for _ in 0 ..< neededLinebreaks {
|
|
insertLinebreak(at: typeDeclaration.openBraceIndex + 1)
|
|
}
|
|
}
|
|
} else if typeExceedsThresholdToAddMarks {
|
|
// Otherwise, this declaration shouldn't have separators.
|
|
// If the type is under the mark threshold, preserve any marks that were added manually.
|
|
removeExistingCategorySeparators(
|
|
from: declaration,
|
|
previousDeclaration: index == 0 ? nil : sortedDeclarations[index - 1].declaration,
|
|
order: order
|
|
)
|
|
}
|
|
|
|
if options.blankLineAfterSubgroups,
|
|
let lastIndexOfSameDeclaration = sortedDeclarations.map(\.category).lastIndex(of: category),
|
|
lastIndexOfSameDeclaration == index,
|
|
lastIndexOfSameDeclaration != sortedDeclarations.indices.last
|
|
{
|
|
declaration.addTrailingBlankLineIfNeeded()
|
|
}
|
|
}
|
|
|
|
// If the type was originally below the MARK threshold, but now meets the MARK threshold after being organized,
|
|
// ensure we do add the marks. Otherwise the marks would just be added next the this rule is ran.
|
|
if !typeExceedsThresholdToAddMarks,
|
|
typeLengthExceedsMarkThreshold(at: typeDeclaration.keywordIndex)
|
|
{
|
|
addCategorySeparators(to: sortedDeclarations, in: typeDeclaration, order: order)
|
|
}
|
|
}
|
|
|
|
/// Removes any existing category separators from the given declarations
|
|
func removeExistingCategorySeparators(
|
|
from declaration: Declaration,
|
|
previousDeclaration: Declaration?,
|
|
order: ParsedOrder,
|
|
preserving shouldPreserveComment: (_ commentBody: String) -> Bool = { _ in false }
|
|
) {
|
|
var matchingComments = matchingCategorySeparatorComments(in: declaration.leadingCommentRange, order: order)
|
|
.map { $0.autoUpdating(in: self) }
|
|
var preservedComment = false
|
|
|
|
while !matchingComments.isEmpty {
|
|
let commentRange = matchingComments.removeFirst()
|
|
|
|
// Preserve the first comment matching the given closure
|
|
if !preservedComment,
|
|
let commentBody = index(after: commentRange.lowerBound, where: \.isCommentBody),
|
|
shouldPreserveComment(tokens[commentBody].string)
|
|
{
|
|
preservedComment = true
|
|
continue
|
|
}
|
|
|
|
// Makes sure there are only whitespace or other comments before this comment.
|
|
// Otherwise, we don't want to remove it.
|
|
let tokensBeforeComment = tokens[declaration.range.lowerBound ..< commentRange.lowerBound]
|
|
guard !tokensBeforeComment.contains(where: { !$0.isSpaceOrCommentOrLinebreak }),
|
|
let nextNonwhitespaceIndex = index(of: .nonSpaceOrLinebreak, after: commentRange.upperBound)
|
|
else {
|
|
continue
|
|
}
|
|
|
|
// If we found a matching comment, remove it and all subsequent empty lines
|
|
let startOfCommentLine = startOfLine(at: commentRange.lowerBound)
|
|
let startOfNextDeclaration = startOfLine(at: nextNonwhitespaceIndex)
|
|
let rangeToRemove = startOfCommentLine ..< startOfNextDeclaration
|
|
removeTokens(in: rangeToRemove)
|
|
|
|
// Move any tokens from before the category separator into the previous declaration.
|
|
// This makes sure that things like comments stay grouped in the same category.
|
|
// Don't do this is we preserved a previous comment, since this following comment is no longer the first one.
|
|
if let previousDeclaration, startOfCommentLine != 0, !preservedComment {
|
|
// Remove the tokens before the category separator from this declaration...
|
|
let rangeBeforeComment = min(startOfCommentLine, declaration.range.lowerBound) ..< startOfCommentLine
|
|
let tokensBeforeCommentLine = Array(tokens[rangeBeforeComment])
|
|
removeTokens(in: rangeBeforeComment)
|
|
|
|
// ... and append them to the end of the previous declaration
|
|
previousDeclaration.append(tokensBeforeCommentLine)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The set of category separate comments like `// MARK: - Public` in the given range.
|
|
/// Looks for approximate matches using edit distance, not exact matches.
|
|
func matchingCategorySeparatorComments(in range: Range<Int>, order: ParsedOrder) -> [ClosedRange<Int>] {
|
|
switch options.typeBodyMarks {
|
|
case .remove:
|
|
return singleLineComments(in: range, matching: { commentBody in
|
|
commentBody.uppercased().trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("MARK:")
|
|
})
|
|
|
|
case .preserve:
|
|
// Current amount of variants to pair visibility-type is over 300,
|
|
// so we take only categories that could provide typemark that we want to erase
|
|
let potentialCategorySeparatorCommentBodies = (
|
|
VisibilityCategory.allCases.map { Category(visibility: $0, type: .classMethod, order: 0) }
|
|
+ DeclarationType.allCases.map { Category(visibility: .visibility(.open), type: $0, order: 0) }
|
|
+ DeclarationType.allCases.map { Category(visibility: .explicit($0), type: .classMethod, order: 0) }
|
|
+ order.filter { $0.comment != nil }
|
|
).flatMap {
|
|
Array(Set([
|
|
// The user's specific category separator template
|
|
$0.markCommentBody(from: options.categoryMarkComment, with: options.organizationMode),
|
|
// Always look for MARKs even if the user is using a different template
|
|
$0.markCommentBody(from: "MARK: %c", with: options.organizationMode),
|
|
]))
|
|
}.compactMap { $0 }
|
|
|
|
return singleLineComments(in: range, matching: { commentBody in
|
|
// Check if this comment matches an expected category separator comment
|
|
for potentialSeparatorCommentBody in potentialCategorySeparatorCommentBodies {
|
|
let existingComment = "// \(commentBody)".lowercased()
|
|
let potentialMatch = "// \(potentialSeparatorCommentBody)".lowercased()
|
|
|
|
// Check the edit distance of this existing comment with the potential
|
|
// valid category separators for this category. If they are similar or identical,
|
|
// we'll want to replace the existing comment with the correct comment.
|
|
let minimumEditDistance = Int(0.2 * Float(existingComment.count))
|
|
|
|
if existingComment.editDistance(from: potentialMatch) <= minimumEditDistance {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Preserves the original spacing for groups of properties that were originally consecutive.
|
|
/// After sorting, only the final declaration in the group should be followed by a blank line.
|
|
func preserveConsecutivePropertyGroupSpacing(
|
|
in typeDeclaration: TypeDeclaration,
|
|
groups consecutiveGroups: [[Declaration]],
|
|
order: ParsedOrder
|
|
) {
|
|
for consecutiveGroup in consecutiveGroups {
|
|
guard let lastDeclarationInOriginalOrder = consecutiveGroup.last,
|
|
let lastDeclarationInSortedBody = typeDeclaration.body.last(where: { declaration in
|
|
consecutiveGroup.contains(where: { $0 === declaration })
|
|
}),
|
|
// If the last declaration was the same both before and after sorting,
|
|
// then the spacing doesn't need to be updated.
|
|
lastDeclarationInOriginalOrder !== lastDeclarationInSortedBody
|
|
else { continue }
|
|
|
|
// Ensure the group as a whole ends in a trailing blank line
|
|
lastDeclarationInSortedBody.addTrailingBlankLineIfNeeded()
|
|
|
|
// The last declaration in the original ordering might have a
|
|
// trailing blank line which is no longer necessary.
|
|
if let declarationIndex = typeDeclaration.body.firstIndex(where: { $0 === lastDeclarationInOriginalOrder }),
|
|
declarationIndex != typeDeclaration.body.indices.last
|
|
{
|
|
let followingDeclaration = typeDeclaration.body[declarationIndex + 1]
|
|
|
|
let thisCategory = category(of: lastDeclarationInOriginalOrder, using: order)
|
|
let followingCategory = category(of: followingDeclaration, using: order)
|
|
|
|
// A trailing blank line is still necessary if the following
|
|
// declaration belongs to a different subgroup or category.
|
|
let mustPreserveBlankLine: Bool
|
|
if options.blankLineAfterSubgroups {
|
|
mustPreserveBlankLine = thisCategory != followingCategory
|
|
} else {
|
|
switch options.organizationMode {
|
|
case .visibility:
|
|
mustPreserveBlankLine = thisCategory.visibility != followingCategory.visibility
|
|
case .type:
|
|
mustPreserveBlankLine = thisCategory.type != followingCategory.type
|
|
}
|
|
}
|
|
|
|
if !mustPreserveBlankLine {
|
|
lastDeclarationInOriginalOrder.removeTrailingBlankLinesIfPresent()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Finds all of the consecutive groups of property declarations in the type body
|
|
func consecutivePropertyDeclarationGroups(in typeDeclaration: TypeDeclaration) -> [[Declaration]] {
|
|
var declarationGroups: [[Declaration]] = []
|
|
var currentGroup: [Declaration] = []
|
|
|
|
/// Ends the current group, ensuring that groups are only recorded
|
|
/// when they contain two or more declarations.
|
|
func endCurrentGroup(addingToExistingGroup declarationToAdd: Declaration? = nil) {
|
|
if let declarationToAdd {
|
|
currentGroup.append(declarationToAdd)
|
|
}
|
|
|
|
if currentGroup.count >= 2 {
|
|
declarationGroups.append(currentGroup)
|
|
}
|
|
|
|
currentGroup = []
|
|
}
|
|
|
|
for declaration in typeDeclaration.body {
|
|
guard declaration.keyword == "let" || declaration.keyword == "var" else {
|
|
endCurrentGroup()
|
|
continue
|
|
}
|
|
|
|
let hasTrailingBlankLine = declaration.tokens.numberOfTrailingLinebreaks() > 1
|
|
|
|
if hasTrailingBlankLine {
|
|
endCurrentGroup(addingToExistingGroup: declaration)
|
|
} else {
|
|
currentGroup.append(declaration)
|
|
}
|
|
}
|
|
|
|
endCurrentGroup()
|
|
return declarationGroups
|
|
}
|
|
}
|
|
|
|
// MARK: - Category
|
|
|
|
/// A category of declarations used by the `organizeDeclarations` rule
|
|
struct Category: Equatable, Hashable {
|
|
var visibility: VisibilityCategory
|
|
var type: DeclarationType
|
|
var order: Int
|
|
var comment: String?
|
|
|
|
/// The comment tokens that should precede all declarations in this category
|
|
func markCommentBody(from template: String, with mode: DeclarationOrganizationMode) -> String? {
|
|
template.replacingOccurrences(
|
|
of: "%c",
|
|
with: comment ?? (mode == .type ? type.markComment : visibility.markComment)
|
|
)
|
|
}
|
|
|
|
/// Whether or not a mark comment should be added for this category,
|
|
/// given the set of existing categories with existing mark comments
|
|
func shouldBeMarked(in categoriesWithMarkComment: Set<Category>, for mode: DeclarationOrganizationMode) -> Bool {
|
|
guard type != .beforeMarks else {
|
|
return false
|
|
}
|
|
|
|
switch mode {
|
|
case .type:
|
|
return !categoriesWithMarkComment.contains(where: { $0.type == type || $0.visibility == .explicit(type) })
|
|
case .visibility:
|
|
return !categoriesWithMarkComment.contains(where: { $0.visibility == visibility })
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The visibility category of a declaration
|
|
///
|
|
/// - Note: When adding a new visibility type, remember to also update the list in `Examples.swift`.
|
|
enum VisibilityCategory: CaseIterable, Hashable, RawRepresentable {
|
|
case visibility(Visibility)
|
|
case explicit(DeclarationType)
|
|
|
|
init?(rawValue: String) {
|
|
if let visibility = Visibility(rawValue: rawValue) {
|
|
self = .visibility(visibility)
|
|
} else if let type = DeclarationType(rawValue: rawValue) {
|
|
self = .explicit(type)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var rawValue: String {
|
|
switch self {
|
|
case let .visibility(visibility):
|
|
return visibility.rawValue
|
|
case let .explicit(declarationType):
|
|
return declarationType.rawValue
|
|
}
|
|
}
|
|
|
|
var markComment: String {
|
|
switch self {
|
|
case let .visibility(type):
|
|
return type.rawValue.capitalized
|
|
case let .explicit(type):
|
|
return type.markComment
|
|
}
|
|
}
|
|
|
|
static var allCases: [VisibilityCategory] {
|
|
Visibility.allCases.map { .visibility($0) }
|
|
}
|
|
|
|
static var essentialCases: [VisibilityCategory] {
|
|
Visibility.allCases.map { .visibility($0) }
|
|
}
|
|
|
|
static func defaultOrdering(for mode: DeclarationOrganizationMode) -> [VisibilityCategory] {
|
|
switch mode {
|
|
case .type:
|
|
return allCases
|
|
case .visibility:
|
|
return [
|
|
.explicit(.beforeMarks),
|
|
.explicit(.instanceLifecycle),
|
|
] + allCases
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Formatter {
|
|
/// The `Category` of the given `Declaration`
|
|
func category(
|
|
of declaration: Declaration,
|
|
using order: ParsedOrder
|
|
) -> Category {
|
|
let visibility = declaration.visibility() ?? .internal
|
|
|
|
let type = declaration.declarationType(
|
|
allowlist: order.map(\.type),
|
|
beforeMarks: options.beforeMarks,
|
|
lifecycleMethods: options.lifecycleMethods
|
|
)
|
|
|
|
let visibilityCategory: VisibilityCategory
|
|
switch options.organizationMode {
|
|
case .visibility:
|
|
guard VisibilityCategory.allCases.contains(.explicit(type)) else {
|
|
fallthrough
|
|
}
|
|
|
|
visibilityCategory = .explicit(type)
|
|
case .type:
|
|
visibilityCategory = .visibility(visibility)
|
|
}
|
|
|
|
return category(from: order, for: visibilityCategory, with: type)
|
|
}
|
|
|
|
typealias ParsedOrder = [Category]
|
|
|
|
/// The ordering of categories to use for the given `DeclarationOrganizationMode`
|
|
func categoryOrder(for mode: DeclarationOrganizationMode) -> ParsedOrder {
|
|
typealias ParsedVisibilityMarks = [VisibilityCategory: String]
|
|
typealias ParsedTypeMarks = [DeclarationType: String]
|
|
|
|
let VisibilityCategorys = options.visibilityOrder?.compactMap { VisibilityCategory(rawValue: $0) }
|
|
?? VisibilityCategory.defaultOrdering(for: mode)
|
|
|
|
let declarationTypes = options.typeOrder?.compactMap { DeclarationType(rawValue: $0) }
|
|
?? DeclarationType.defaultOrdering(for: mode)
|
|
|
|
// Validate that every essential declaration type is included in either `declarationTypes` or `VisibilityCategorys`.
|
|
// Otherwise, we will just crash later when we find a declaration with this type.
|
|
for essentialDeclarationType in DeclarationType.essentialCases {
|
|
guard declarationTypes.contains(essentialDeclarationType)
|
|
|| VisibilityCategorys.contains(.explicit(essentialDeclarationType))
|
|
else {
|
|
Swift.fatalError("\(essentialDeclarationType.rawValue) must be included in either --type-order or --visibility-order")
|
|
}
|
|
}
|
|
|
|
let customVisibilityMarks = options.customVisibilityMarks
|
|
let customTypeMarks = options.customTypeMarks
|
|
|
|
let parsedVisibilityMarks: ParsedVisibilityMarks = parseMarks(for: customVisibilityMarks)
|
|
let parsedTypeMarks: ParsedTypeMarks = parseMarks(for: customTypeMarks)
|
|
|
|
switch mode {
|
|
case .visibility:
|
|
let categoryPairings = VisibilityCategorys.flatMap { VisibilityCategory -> [(VisibilityCategory, DeclarationType)] in
|
|
switch VisibilityCategory {
|
|
case let .visibility(visibility):
|
|
// Each visibility / access control level pairs with all of the declaration types
|
|
return declarationTypes.compactMap { declarationType in
|
|
(.visibility(visibility), declarationType)
|
|
}
|
|
|
|
case let .explicit(explicitDeclarationType):
|
|
// Each top-level declaration category pairs with all of the visibility types
|
|
return VisibilityCategorys.map { VisibilityCategory in
|
|
(VisibilityCategory, explicitDeclarationType)
|
|
}
|
|
}
|
|
}
|
|
|
|
return categoryPairings.enumerated().map { offset, element in
|
|
Category(
|
|
visibility: element.0,
|
|
type: element.1,
|
|
order: offset,
|
|
comment: parsedVisibilityMarks[element.0]
|
|
)
|
|
}
|
|
|
|
case .type:
|
|
let categoryPairings = declarationTypes.flatMap { declarationType -> [(VisibilityCategory, DeclarationType)] in
|
|
VisibilityCategorys.map { VisibilityCategory in
|
|
(VisibilityCategory, declarationType)
|
|
}
|
|
}
|
|
|
|
return categoryPairings.enumerated().map { offset, element in
|
|
Category(
|
|
visibility: element.0,
|
|
type: element.1,
|
|
order: offset,
|
|
comment: parsedTypeMarks[element.1]
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The `Category` of a declaration with the given `VisibilityCategory` and `DeclarationType`
|
|
func category(
|
|
from order: ParsedOrder,
|
|
for visibility: VisibilityCategory,
|
|
with type: DeclarationType
|
|
) -> Category {
|
|
guard let category = order.first(where: { entry in
|
|
entry.visibility == visibility && entry.type == type
|
|
|| (entry.visibility == .explicit(type) && entry.type == type)
|
|
})
|
|
else {
|
|
Swift.fatalError("Cannot determine ordering for declaration with visibility=\(visibility.rawValue) and type=\(type.rawValue).")
|
|
}
|
|
|
|
return category
|
|
}
|
|
|
|
func parseMarks<T: RawRepresentable>(
|
|
for options: Set<String>
|
|
) -> [T: String] where T.RawValue == String {
|
|
options.map { customMarkEntry -> (T, String)? in
|
|
let split = customMarkEntry.split(separator: ":", maxSplits: 1)
|
|
|
|
guard split.count == 2,
|
|
let rawValue = split.first,
|
|
let mark = split.last,
|
|
let concreteType = T(rawValue: String(rawValue))
|
|
else { return nil }
|
|
|
|
return (concreteType, String(mark))
|
|
}
|
|
.compactMap { $0 }
|
|
.reduce(into: [:]) { dictionary, option in
|
|
dictionary[option.0] = option.1
|
|
}
|
|
}
|
|
|
|
func typeLengthExceedsMarkThreshold(at typeKeywordIndex: Int) -> Bool {
|
|
let markThreshold: Int
|
|
switch tokens[typeKeywordIndex].string {
|
|
case "class", "actor":
|
|
markThreshold = options.markClassThreshold
|
|
case "struct":
|
|
markThreshold = options.markStructThreshold
|
|
case "enum":
|
|
markThreshold = options.markEnumThreshold
|
|
case "extension":
|
|
markThreshold = options.markExtensionThreshold
|
|
default:
|
|
markThreshold = 0
|
|
}
|
|
guard markThreshold != 0,
|
|
let startOfScope = index(of: .startOfScope("{"), after: typeKeywordIndex),
|
|
let endOfScope = endOfScope(at: startOfScope)
|
|
else {
|
|
return true
|
|
}
|
|
let lineCount = tokens[startOfScope ... endOfScope]
|
|
.filter(\.isLinebreak)
|
|
.count
|
|
- 1
|
|
return lineCount >= markThreshold
|
|
}
|
|
}
|
|
|
|
extension Array where Element: Equatable & Hashable {
|
|
/// Sort function to sort an array based on the order of the elements on Self
|
|
/// - Parameters:
|
|
/// - lhs: Sort closure left hand side element
|
|
/// - rhs: Sort closure right hand side element
|
|
/// - Returns: Whether the elements are sorted or not.
|
|
func areInRelativeOrder(lhs: Element, rhs: Element) -> Bool {
|
|
guard let lhsIndex = firstIndex(of: lhs) else { return false }
|
|
guard let rhsIndex = firstIndex(of: rhs) else { return true }
|
|
return lhsIndex < rhsIndex
|
|
}
|
|
|
|
/// Creates a list without duplicates and ordered by the first time the element appeared in Self
|
|
/// For example, this function would transform [1,2,3,1,2] into [1,2,3]
|
|
func firstElementAppearanceOrder() -> [Element] {
|
|
var appeared: Set<Element> = []
|
|
var appearedList: [Element] = []
|
|
|
|
for element in self {
|
|
if !appeared.contains(element) {
|
|
appeared.insert(element)
|
|
appearedList.append(element)
|
|
}
|
|
}
|
|
return appearedList
|
|
}
|
|
}
|