Files
SwiftFormat/Sources/Rules/OrganizeDeclarations.swift
T
2025-05-12 20:18:42 -07:00

874 lines
35 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],
options: [
"categorymark", "markcategories", "beforemarks",
"lifecycle", "organizetypes", "structthreshold", "classthreshold",
"enumthreshold", "extensionlength", "organizationmode",
"visibilityorder", "typeorder", "visibilitymarks", "typemarks",
"groupblanklines", "sortswiftuiprops",
],
sharedOptions: ["sortedpatterns", "lineaftermarks", "typeblanklines"]
) { formatter in
guard !formatter.options.fragment else { return }
formatter.mapRecursiveDeclarations { declaration in
switch declaration {
// Organize the body of type declarations
case let .type(kind, open, body, close, originalRange):
let organizedType = formatter.organizeDeclaration((kind, open, body, close))
return .type(
kind: organizedType.kind,
open: organizedType.open,
body: organizedType.body,
close: organizedType.close,
originalRange: originalRange
)
case .conditionalCompilation, .declaration:
return declaration
}
}
} examples: {
"""
Default value for `--visibilityorder` when using `--organizationmode visibility`:
`\(VisibilityCategory.defaultOrdering(for: .visibility).map(\.rawValue).joined(separator: ", "))`
Default value for `--visibilityorder` when using `--organizationmode type`:
`\(VisibilityCategory.defaultOrdering(for: .type).map(\.rawValue).joined(separator: ", "))`
**NOTE:** When providing custom arguments for `--visibilityorder` the following entries must be included:
`\(VisibilityCategory.essentialCases.map(\.rawValue).joined(separator: ", "))`
Default value for `--typeorder` when using `--organizationmode visibility`:
`\(DeclarationType.defaultOrdering(for: .visibility).map(\.rawValue).joined(separator: ", "))`
Default value for `--typeorder` when using `--organizationmode type`:
`\(DeclarationType.defaultOrdering(for: .type).map(\.rawValue).joined(separator: ", "))`
**NOTE:** The follow declaration types must be included in either `--typeorder` or `--visibilityorder`:
`\(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.
`--organizationmode 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
+
}
```
`--organizationmode 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 {
/// A `Declaration` that represents a Swift type
typealias TypeDeclaration = (kind: String, open: [Token], body: [Declaration], close: [Token])
/// Organizes the given type declaration into sorted categories
func organizeDeclaration(_ typeDeclaration: TypeDeclaration) -> TypeDeclaration {
guard options.organizeTypes.contains(typeDeclaration.kind),
typeLengthExceedsOrganizationThreshold(typeDeclaration)
else { return typeDeclaration }
// Parse category order from options
let categoryOrder = self.categoryOrder(for: options.organizationMode)
// Remove all of the existing category separators, so they can be re-added
// at the correct location after sorting the declarations.
var typeBody = removeExistingCategorySeparators(
from: typeDeclaration.body,
with: options.organizationMode,
using: categoryOrder
)
// Track the consecutive groups of property declarations so we can avoid inserting
// blank lines between elements in the group if possible.
var consecutivePropertyGroups = consecutivePropertyDeclarationGroups(in: typeDeclaration.body)
.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
}
// Remove the trailing blank line from the last declaration in each consecutive group
for (groupIndex, consecutivePropertyGroup) in consecutivePropertyGroups.enumerated() {
guard let lastDeclarationInGroup = consecutivePropertyGroup.last,
let indexOfDeclaration = typeBody.firstIndex(of: lastDeclarationInGroup)
else { continue }
let updatedDeclaration = lastDeclarationInGroup.endingWithoutBlankLine()
let indexInGroup = consecutivePropertyGroup.indices.last!
typeBody[indexOfDeclaration] = updatedDeclaration
consecutivePropertyGroups[groupIndex][indexInGroup] = updatedDeclaration
}
// Categorize each of the declarations into their primary groups
let categorizedDeclarations: [CategorizedDeclaration] = typeBody
.map { declaration in
let declarationCategory = category(
of: declaration,
for: options.organizationMode,
using: categoryOrder
)
return (declaration: declaration, category: declarationCategory)
}
// Sort the declarations based on their category and type
guard var sortedTypeBody = sortCategorizedDeclarations(
categorizedDeclarations,
in: typeDeclaration
)
else { return typeDeclaration }
// Insert a blank line after the last declaration in each original group
for consecutivePropertyGroup in consecutivePropertyGroups {
let propertiesInGroup = Set(consecutivePropertyGroup)
guard let lastDeclarationInSortedBody = sortedTypeBody.lastIndex(where: { propertiesInGroup.contains($0.declaration) })
else { continue }
sortedTypeBody[lastDeclarationInSortedBody].declaration =
sortedTypeBody[lastDeclarationInSortedBody].declaration.endingWithBlankLine()
}
// Add a mark comment for each top-level category
let sortedAndMarkedType = addCategorySeparators(
to: typeDeclaration,
sortedDeclarations: sortedTypeBody
)
return sortedAndMarkedType
}
/// Whether or not the length of this types exceeds the minimum threshold to be organized
func typeLengthExceedsOrganizationThreshold(_ typeDeclaration: TypeDeclaration) -> Bool {
let organizationThreshold: Int
switch typeDeclaration.kind {
case "class", "actor":
organizationThreshold = options.organizeClassThreshold
case "struct":
organizationThreshold = options.organizeStructThreshold
case "enum":
organizationThreshold = options.organizeEnumThreshold
case "extension":
organizationThreshold = options.organizeExtensionThreshold
default:
organizationThreshold = 0
}
guard organizationThreshold != 0 else {
return true
}
let lineCount = typeDeclaration.body
.flatMap(\.tokens)
.filter(\.isLinebreak)
.count
return lineCount >= organizationThreshold
}
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 take care to not reorder any properties (but reordering functions etc is ok!)
if !sortAlphabeticallyWithinSubcategories, typeDeclaration.kind == "struct",
!typeDeclaration.body.contains(where: { $0.keyword == "init" }),
!preservesSynthesizedMemberwiseInitializer(categorizedDeclarations, sortedDeclarations)
{
// If sorting by category and by type could cause compilation failures
// by not correctly preserving the synthesized memberwise initializer,
// try to sort _only_ by category (so we can try to preserve the correct category separators)
sortedDeclarations = sortDeclarations(categorizedDeclarations, sortAlphabeticallyWithinSubcategories: false)
// If sorting _only_ by category still changes the synthesized memberwise initializer,
// then there's nothing we can do to organize this struct.
if !preservesSynthesizedMemberwiseInitializer(categorizedDeclarations, sortedDeclarations) {
return nil
}
}
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 let swiftUIPropertiesSortMode = options.swiftUIPropertiesSortMode,
lhs.category.type == rhs.category.type,
let lhsSwiftUIProperty = lhs.declaration.swiftUIPropertyWrapper,
let rhsSwiftUIProperty = rhs.declaration.swiftUIPropertyWrapper
{
switch swiftUIPropertiesSortMode {
case .alphabetize:
return lhsSwiftUIProperty.localizedCompare(rhsSwiftUIProperty) == .orderedAscending
case .firstAppearanceSort:
return customDeclarationSortOrder.areInRelativeOrder(lhs: lhsSwiftUIProperty, rhs: rhsSwiftUIProperty)
}
} else {
// 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 {
// If this type has a leading :sort directive, we sort alphabetically
// within the subcategories (where ordering is otherwise undefined)
let shouldSortAlphabeticallyBySortingMark = typeDeclaration.open.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 = {
let parser = Formatter(typeDeclaration.open)
guard let kindIndex = parser.index(of: .keyword(typeDeclaration.kind), in: 0 ..< typeDeclaration.open.count),
let identifier = parser.next(.identifier, after: kindIndex)
else {
return false
}
return options.alphabeticallySortedDeclarationPatterns.contains {
identifier.string.contains($0)
}
}()
return shouldSortAlphabeticallyBySortingMark
|| shouldSortAlphabeticallyByDeclarationPattern
}
// Whether or not this declaration is an instance property that can affect
// the parameters struct's synthesized memberwise initializer
func affectsSynthesizedMemberwiseInitializer(_ declaration: Declaration) -> Bool {
declaration.isStoredInstanceProperty
}
// 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 { affectsSynthesizedMemberwiseInitializer($0.declaration) }
.map(\.declaration)
let rhsPropertiesOrder = rhs
.filter { affectsSynthesizedMemberwiseInitializer($0.declaration) }
.map(\.declaration)
return lhsPropertiesOrder == rhsPropertiesOrder
}
// Finds all of the consecutive groups of property declarations in the type body
func consecutivePropertyDeclarationGroups(in body: [Declaration]) -> [[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 = declarationToAdd {
currentGroup.append(declarationToAdd)
}
if currentGroup.count >= 2 {
declarationGroups.append(currentGroup)
}
currentGroup = []
}
for declaration in 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
}
/// Adds MARK category separates to the given type
func addCategorySeparators(
to typeDeclaration: TypeDeclaration,
sortedDeclarations: [CategorizedDeclaration]
) -> TypeDeclaration {
let numberOfCategories: Int = {
switch options.organizationMode {
case .visibility:
return Set(sortedDeclarations.map(\.category).map(\.visibility)).count
case .type:
return Set(sortedDeclarations.map(\.category).map(\.type)).count
}
}()
var typeDeclaration = typeDeclaration
var formattedCategories: [Category] = []
var markedDeclarations: [Declaration] = []
for (index, (declaration, category)) in sortedDeclarations.enumerated() {
if options.markCategories,
numberOfCategories > 1,
let markComment = category.markComment(from: options.categoryMarkComment, with: options.organizationMode),
category.shouldBeMarked(in: Set(formattedCategories), for: options.organizationMode)
{
formattedCategories.append(category)
let declarationParser = Formatter(declaration.tokens)
let indentation = declarationParser.currentIndentForLine(at: 0)
let endMarkDeclaration = options.lineAfterMarks ? "\n\n" : "\n"
let markDeclaration = tokenize("\(indentation)\(markComment)\(endMarkDeclaration)")
// 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 category separator appears balanced
if markedDeclarations.isEmpty {
if options.enabledRules.contains(FormatRule.blankLinesAtStartOfScope.name),
options.removeStartOrEndBlankLinesFromTypes
{
typeDeclaration.open = typeDeclaration.open.endingWithoutBlankLine()
} else {
typeDeclaration.open = typeDeclaration.open.endingWithBlankLine()
}
}
markedDeclarations.append(.declaration(
kind: "comment",
tokens: markDeclaration,
originalRange: 0 ... 1 // placeholder value
))
}
if options.blankLineAfterSubgroups,
let lastIndexOfSameDeclaration = sortedDeclarations.map(\.category).lastIndex(of: category),
lastIndexOfSameDeclaration == index,
lastIndexOfSameDeclaration != sortedDeclarations.indices.last
{
markedDeclarations.append(declaration.endingWithBlankLine())
} else {
markedDeclarations.append(declaration)
}
}
typeDeclaration.body = markedDeclarations
return typeDeclaration
}
/// Removes any existing category separators from the given declarations
func removeExistingCategorySeparators(
from typeBody: [Declaration],
with mode: DeclarationOrganizationMode,
using order: ParsedOrder
) -> [Declaration] {
var typeBody = typeBody
for (declarationIndex, declaration) in typeBody.enumerated() {
let tokensToInspect: [Token]
switch declaration {
case let .declaration(_, tokens, _):
tokensToInspect = tokens
case let .type(_, open, _, _, _), let .conditionalCompilation(open, _, _, _):
// Only inspect the opening tokens of declarations with a body
tokensToInspect = open
}
// 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 potentialCategorySeparators = (
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.markComment(from: options.categoryMarkComment, with: mode),
// Other common variants that we would want to replace with the correct variant
$0.markComment(from: "%c", with: mode),
$0.markComment(from: "// MARK: %c", with: mode),
]))
}.compactMap { $0 }
let parser = Formatter(tokensToInspect)
parser.forEach(.startOfScope("//")) { commentStartIndex, _ in
// Only look at top-level comments inside of the type body
guard parser.currentScope(at: commentStartIndex) == nil else {
return
}
// Check if this comment matches an expected category separator comment
for potentialSeparatorComment in potentialCategorySeparators {
let potentialCategorySeparator = tokenize(potentialSeparatorComment)
let potentialSeparatorRange = commentStartIndex ..< (commentStartIndex + potentialCategorySeparator.count)
guard parser.tokens.indices.contains(potentialSeparatorRange.upperBound),
let nextNonwhitespaceIndex = parser.index(of: .nonSpaceOrLinebreak, after: potentialSeparatorRange.upperBound)
else { continue }
// 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 existingComment = sourceCode(for: Array(parser.tokens[potentialSeparatorRange]))
let minimumEditDistance = Int(0.2 * Float(existingComment.count))
guard existingComment.lowercased().editDistance(from: potentialSeparatorComment.lowercased())
<= minimumEditDistance
else { continue }
// Makes sure there are only whitespace or other comments before this comment.
// Otherwise, we don't want to remove it.
let tokensBeforeComment = parser.tokens[0 ..< commentStartIndex]
guard !tokensBeforeComment.contains(where: { !$0.isSpaceOrCommentOrLinebreak }) else {
continue
}
// If we found a matching comment, remove it and all subsequent empty lines
let startOfCommentLine = parser.startOfLine(at: commentStartIndex)
let startOfNextDeclaration = parser.startOfLine(at: nextNonwhitespaceIndex)
parser.removeTokens(in: startOfCommentLine ..< startOfNextDeclaration)
// 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.
if declarationIndex != 0, startOfCommentLine != 0 {
// Remove the tokens before the category separator from this declaration...
let rangeBeforeComment = 0 ..< startOfCommentLine
let tokensBeforeCommentLine = Array(parser.tokens[rangeBeforeComment])
parser.removeTokens(in: rangeBeforeComment)
// ... and append them to the end of the previous declaration
typeBody[declarationIndex - 1] = typeBody[declarationIndex - 1].mapClosingTokens {
$0 + tokensBeforeCommentLine
}
}
// Apply the updated tokens back to this declaration
typeBody[declarationIndex] = typeBody[declarationIndex].mapOpeningTokens { _ in
parser.tokens
}
}
}
}
return typeBody
}
}
// 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? = nil
/// The comment tokens that should precede all declarations in this category
func markComment(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,
for mode: DeclarationOrganizationMode,
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 mode {
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 --typeorder or --visibilityorder")
}
}
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
}
}
}
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
}
}