mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
122 lines
5.9 KiB
Swift
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)
|
|
}
|
|
}
|