mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
Fix extensionAccessControl incorrectly hoisting public onto extensions of nested internal types (#2461)
Co-authored-by: Cal Stephens <cal@calstephens.tech>
This commit is contained in:
@@ -14,6 +14,22 @@ public extension FormatRule {
|
||||
options: ["extension-acl"]
|
||||
) { formatter in
|
||||
let declarations = formatter.parseDeclarations()
|
||||
|
||||
// Build a map of fully-qualified type names to their effective visibility.
|
||||
var typeVisibilityByName = [String: Visibility]()
|
||||
declarations.forEachRecursiveDeclaration { declaration in
|
||||
guard declaration.keyword != "extension",
|
||||
declaration.asTypeDeclaration != nil,
|
||||
let qualifiedName = declaration.fullyQualifiedName
|
||||
else { return }
|
||||
|
||||
// A type declared inside a `public extension` inherits public visibility.
|
||||
let insidePublicExtension = declaration.parentDeclarations.contains(where: {
|
||||
$0.keyword == "extension" && $0.visibility() == .public
|
||||
})
|
||||
typeVisibilityByName[qualifiedName] = insidePublicExtension ? .public : (declaration.visibility() ?? .internal)
|
||||
}
|
||||
|
||||
declarations.forEachRecursiveDeclaration { declaration in
|
||||
guard let extensionDeclaration = declaration.asTypeDeclaration,
|
||||
extensionDeclaration.keyword == "extension"
|
||||
@@ -54,18 +70,13 @@ public extension FormatRule {
|
||||
else { return }
|
||||
|
||||
if memberVisibility > extensionVisibility ?? .internal {
|
||||
// Check type being extended does not have lower visibility
|
||||
for extendedType in declarations where extendedType.name == extensionDeclaration.name {
|
||||
guard let type = extendedType.asTypeDeclaration else { continue }
|
||||
|
||||
if extendedType.keyword != "extension",
|
||||
extendedType.visibility() ?? .internal < memberVisibility
|
||||
{
|
||||
// Cannot make extension with greater visibility than type being extended
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
// Check the type being extended does not have lower visibility.
|
||||
if let extendedTypeName = extensionDeclaration.name,
|
||||
let typeVisibility = typeVisibilityByName[extendedTypeName],
|
||||
typeVisibility < memberVisibility
|
||||
{
|
||||
// Cannot make extension with greater visibility than type being extended
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -450,6 +450,58 @@ final class ExtensionAccessControlTests: XCTestCase {
|
||||
testFormatting(for: input, rule: .extensionAccessControl, exclude: [.redundantPublic])
|
||||
}
|
||||
|
||||
func testAccessNotHoistedIfNestedTypeVisibilityIsLower() {
|
||||
// Extension of a dot-separated nested type whose inner type is internal.
|
||||
// SwiftFormat must not hoist `public` onto the extension because the type is internal.
|
||||
let input = """
|
||||
extension CategorySurface {
|
||||
struct RetailerItemGroup: Hashable {
|
||||
let collection: String
|
||||
}
|
||||
}
|
||||
|
||||
extension CategorySurface.RetailerItemGroup {
|
||||
public static func placeholder(id: String) -> Self {
|
||||
.init(collection: id)
|
||||
}
|
||||
}
|
||||
"""
|
||||
testFormatting(for: input, rule: .extensionAccessControl, exclude: [.redundantPublic])
|
||||
}
|
||||
|
||||
func testAccessHoistedForPublicNestedType() {
|
||||
// Extension of a dot-separated nested type that IS public should still allow hoisting.
|
||||
let input = """
|
||||
extension CategorySurface {
|
||||
public struct RetailerItemGroup: Hashable {
|
||||
let collection: String
|
||||
}
|
||||
}
|
||||
|
||||
extension CategorySurface.RetailerItemGroup {
|
||||
public static func placeholder(id: String) -> Self {
|
||||
.init(collection: id)
|
||||
}
|
||||
}
|
||||
"""
|
||||
// `public` is hoisted from `public struct RetailerItemGroup` to `public extension CategorySurface`,
|
||||
// and from `public static func placeholder` to `public extension CategorySurface.RetailerItemGroup`.
|
||||
let output = """
|
||||
public extension CategorySurface {
|
||||
struct RetailerItemGroup: Hashable {
|
||||
let collection: String
|
||||
}
|
||||
}
|
||||
|
||||
public extension CategorySurface.RetailerItemGroup {
|
||||
static func placeholder(id: String) -> Self {
|
||||
.init(collection: id)
|
||||
}
|
||||
}
|
||||
"""
|
||||
testFormatting(for: input, output, rule: .extensionAccessControl)
|
||||
}
|
||||
|
||||
func testExtensionAccessControlRuleTerminatesInFileWithConditionalCompilation() {
|
||||
let input = """
|
||||
#if os(Linux)
|
||||
|
||||
Reference in New Issue
Block a user