Files
SwiftFormat/Sources/Rules/ExtensionAccessControl.swift
T
2024-11-09 11:26:22 +00:00

122 lines
5.9 KiB
Swift

//
// ExtensionAccessControl.swift
// SwiftFormat
//
// Created by Cal Stephens on 9/25/20.
// Copyright © 2024 Nick Lockwood. All rights reserved.
//
import Foundation
public extension FormatRule {
static let extensionAccessControl = FormatRule(
help: "Configure the placement of an extension's access control keyword.",
options: ["extensionacl"]
) { formatter in
guard !formatter.options.fragment else { return }
let declarations = formatter.parseDeclarations()
let updatedDeclarations = formatter.mapRecursiveDeclarations(declarations) { declaration, _ in
guard case let .type("extension", open, body, close, _) = declaration else {
return declaration
}
let visibilityKeyword = formatter.visibility(of: declaration)
// `private` visibility at top level of file is equivalent to `fileprivate`
let extensionVisibility = (visibilityKeyword == .private) ? .fileprivate : visibilityKeyword
switch formatter.options.extensionACLPlacement {
// If all declarations in the extension have the same visibility,
// remove the keyword from the individual declarations and
// place it on the extension itself.
case .onExtension:
if extensionVisibility == nil,
let delimiterIndex = declaration.openTokens.firstIndex(of: .delimiter(":")),
declaration.openTokens.firstIndex(of: .keyword("where")).map({ $0 > delimiterIndex }) ?? true
{
// Extension adds protocol conformance so can't have visibility modifier
return declaration
}
let visibilityOfBodyDeclarations = formatter
.mapDeclarations(body) {
formatter.visibility(of: $0) ?? extensionVisibility ?? .internal
}
.compactMap { $0 }
let counts = Set(visibilityOfBodyDeclarations).sorted().map { visibility in
(visibility, count: visibilityOfBodyDeclarations.filter { $0 == visibility }.count)
}
guard let memberVisibility = counts.max(by: { $0.count < $1.count })?.0,
memberVisibility <= extensionVisibility ?? .public,
// Check that most common level is also most visible
memberVisibility == visibilityOfBodyDeclarations.max(),
// `private` can't be hoisted without changing code behavior
// (private applied at extension level is equivalent to `fileprivate`)
memberVisibility > .private
else { return declaration }
if memberVisibility > extensionVisibility ?? .internal {
// Check type being extended does not have lower visibility
for d in declarations where d.name == declaration.name {
if case let .type(kind, _, _, _, _) = d {
if kind != "extension", formatter.visibility(of: d) ?? .internal < memberVisibility {
// Cannot make extension with greater visibility than type being extended
return declaration
}
break
}
}
}
let extensionWithUpdatedVisibility: Declaration
if memberVisibility == extensionVisibility ||
(memberVisibility == .internal && visibilityKeyword == nil)
{
extensionWithUpdatedVisibility = declaration
} else {
extensionWithUpdatedVisibility = formatter.add(memberVisibility, to: declaration)
}
return formatter.mapBodyDeclarations(in: extensionWithUpdatedVisibility) { bodyDeclaration in
let visibility = formatter.visibility(of: bodyDeclaration)
if memberVisibility > visibility ?? extensionVisibility ?? .internal {
if visibility == nil {
return formatter.add(.internal, to: bodyDeclaration)
}
return bodyDeclaration
}
return formatter.remove(memberVisibility, from: bodyDeclaration)
}
// Move the extension's visibility keyword to each individual declaration
case .onDeclarations:
// If the extension visibility is unspecified then there isn't any work to do
guard let extensionVisibility = extensionVisibility else {
return declaration
}
// Remove the visibility keyword from the extension declaration itself
let extensionWithUpdatedVisibility = formatter.remove(visibilityKeyword!, from: declaration)
// And apply the extension's visibility to each of its child declarations
// that don't have an explicit visibility keyword
return formatter.mapBodyDeclarations(in: extensionWithUpdatedVisibility) { bodyDeclaration in
if formatter.visibility(of: bodyDeclaration) == nil {
// If there was no explicit visibility keyword, then this declaration
// was using the visibility of the extension itself.
return formatter.add(extensionVisibility, to: bodyDeclaration)
} else {
// Keep the existing visibility
return bodyDeclaration
}
}
}
}
let updatedTokens = updatedDeclarations.flatMap { $0.tokens }
formatter.replaceTokens(in: formatter.tokens.indices, with: updatedTokens)
}
}