// // ExtensionAccessControlTests.swift // SwiftFormatTests // // Created by Cal Stephens on 9/25/20. // Copyright © 2024 Nick Lockwood. All rights reserved. // import XCTest @testable import SwiftFormat final class ExtensionAccessControlTests: XCTestCase { func testUpdatesVisibilityOfExtensionMembers() { let input = """ private extension Foo { var publicProperty: Int { 10 } public func publicFunction1() {} func publicFunction2() {} internal func internalFunction() {} private func privateFunction() {} fileprivate var privateProperty: Int { 10 } } """ let output = """ extension Foo { fileprivate var publicProperty: Int { 10 } public func publicFunction1() {} fileprivate func publicFunction2() {} internal func internalFunction() {} private func privateFunction() {} fileprivate var privateProperty: Int { 10 } } """ testFormatting( for: input, output, rule: .extensionAccessControl, options: FormatOptions(extensionACLPlacement: .onDeclarations), exclude: [.redundantInternal, .wrapPropertyBodies] ) } func testUpdatesVisibilityOfExtensionInConditionalCompilationBlock() { let input = """ #if DEBUG public extension Foo { var publicProperty: Int { 10 } } #endif """ let output = """ #if DEBUG extension Foo { public var publicProperty: Int { 10 } } #endif """ testFormatting( for: input, output, rule: .extensionAccessControl, options: FormatOptions(extensionACLPlacement: .onDeclarations), exclude: [.wrapPropertyBodies] ) } func testUpdatesVisibilityOfExtensionMembersInConditionalCompilationBlock() { let input = """ public extension Foo { #if DEBUG var publicProperty: Int { 10 } #endif } """ let output = """ extension Foo { #if DEBUG public var publicProperty: Int { 10 } #endif } """ testFormatting( for: input, output, rule: .extensionAccessControl, options: FormatOptions(extensionACLPlacement: .onDeclarations), exclude: [.wrapPropertyBodies] ) } func testDoesntUpdateDeclarationsInsideTypeInsideExtension() { let input = """ public extension Foo { struct Bar { var baz: Int var quux: Int } } """ let output = """ extension Foo { public struct Bar { var baz: Int var quux: Int } } """ testFormatting( for: input, output, rule: .extensionAccessControl, options: FormatOptions(extensionACLPlacement: .onDeclarations), exclude: [.wrapPropertyBodies] ) } func testDoesNothingForInternalExtension() { let input = """ extension Foo { func bar() {} func baz() {} public func quux() {} } """ testFormatting( for: input, rule: .extensionAccessControl, options: FormatOptions(extensionACLPlacement: .onDeclarations) ) } func testPlacesVisibilityKeywordAfterAnnotations() { let input = """ public extension Foo { @discardableResult func bar() -> Int { 10 } /// Doc comment @discardableResult @available(iOS 10.0, *) func baz() -> Int { 10 } @objc func quux() {} @available(iOS 10.0, *) func quixotic() {} } """ let output = """ extension Foo { @discardableResult public func bar() -> Int { 10 } /// Doc comment @discardableResult @available(iOS 10.0, *) public func baz() -> Int { 10 } @objc public func quux() {} @available(iOS 10.0, *) public func quixotic() {} } """ testFormatting( for: input, output, rule: .extensionAccessControl, options: FormatOptions(extensionACLPlacement: .onDeclarations), exclude: [.wrapFunctionBodies] ) } func testConvertsExtensionPrivateToMemberFileprivate() { let input = """ private extension Foo { var bar: Int } let bar = Foo().bar """ let output = """ extension Foo { fileprivate var bar: Int } let bar = Foo().bar """ testFormatting( for: input, output, rule: .extensionAccessControl, options: FormatOptions(extensionACLPlacement: .onDeclarations, swiftVersion: "4"), exclude: [.propertyTypes] ) } // MARK: extensionAccessControl .onExtension func testUpdatedVisibilityOfExtension() { let input = """ extension Foo { public func bar() {} public var baz: Int { 10 } public struct Foo2 { var quux: Int } } """ let output = """ public extension Foo { func bar() {} var baz: Int { 10 } struct Foo2 { var quux: Int } } """ testFormatting(for: input, output, rule: .extensionAccessControl, exclude: [.wrapPropertyBodies]) } func testUpdatedVisibilityOfExtensionWithDeclarationsInConditionalCompilation() { let input = """ extension Foo { #if DEBUG public func bar() {} public var baz: Int { 10 } #endif } """ let output = """ public extension Foo { #if DEBUG func bar() {} var baz: Int { 10 } #endif } """ testFormatting(for: input, output, rule: .extensionAccessControl, exclude: [.wrapPropertyBodies]) } func testDoesntUpdateExtensionVisibilityWithoutMajorityBodyVisibility() { let input = """ extension Foo { public func foo() {} public func bar() {} var baz: Int { 10 } var quux: Int { 5 } } """ testFormatting(for: input, rule: .extensionAccessControl, exclude: [.wrapPropertyBodies]) } func testUpdateExtensionVisibilityWithMajorityBodyVisibility() { let input = """ extension Foo { public func foo() {} public func bar() {} public var baz: Int { 10 } var quux: Int { 5 } } """ let output = """ public extension Foo { func foo() {} func bar() {} var baz: Int { 10 } internal var quux: Int { 5 } } """ testFormatting(for: input, output, rule: .extensionAccessControl, exclude: [.wrapPropertyBodies]) } func testDoesntUpdateExtensionVisibilityWhenMajorityBodyVisibilityIsntMostVisible() { let input = """ extension Foo { func foo() {} func bar() {} public var baz: Int { 10 } } """ testFormatting(for: input, rule: .extensionAccessControl, exclude: [.wrapPropertyBodies]) } func testDoesntUpdateExtensionVisibilityWithInternalDeclarations() { let input = """ extension Foo { func bar() {} var baz: Int { 10 } } """ testFormatting(for: input, rule: .extensionAccessControl, exclude: [.wrapPropertyBodies]) } func testDoesntUpdateExtensionThatAlreadyHasCorrectVisibilityKeyword() { let input = """ public extension Foo { func bar() {} func baz() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testUpdatesExtensionThatHasHigherACLThanBodyDeclarations() { let input = """ public extension Foo { fileprivate func bar() {} fileprivate func baz() {} } """ let output = """ fileprivate extension Foo { func bar() {} func baz() {} } """ testFormatting(for: input, output, rule: .extensionAccessControl, exclude: [.redundantFileprivate]) } func testDoesntHoistPrivateVisibilityFromExtensionBodyDeclarations() { let input = """ extension Foo { private var bar() {} private func baz() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testDoesntUpdatesExtensionThatHasLowerACLThanBodyDeclarations() { let input = """ private extension Foo { public var bar() {} public func baz() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testDoesntReduceVisibilityOfImplicitInternalDeclaration() { let input = """ extension Foo { fileprivate var bar() {} func baz() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testUpdatesExtensionThatHasRedundantACLOnBodyDeclarations() { let input = """ public extension Foo { func bar() {} public func baz() {} } """ let output = """ public extension Foo { func bar() {} func baz() {} } """ testFormatting(for: input, output, rule: .extensionAccessControl) } func testNoHoistAccessModifierForOpenMethod() { let input = """ extension Foo { open func bar() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testDontChangePrivateExtensionToFileprivate() { let input = """ private extension Foo { func bar() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testDontRemoveInternalKeywordFromExtension() { let input = """ internal extension Foo { func bar() {} } """ testFormatting(for: input, rule: .extensionAccessControl, exclude: [.redundantInternal]) } func testNoHoistAccessModifierForExtensionThatAddsProtocolConformance() { let input = """ extension Foo: Bar { public func bar() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testNoHoistAccessModifierForExtensionThatAddsPreconcurrencyProtocolConformance() { let input = """ extension Foo: @preconcurrency Bar { public func bar() {} } """ testFormatting(for: input, rule: .extensionAccessControl) } func testProtocolConformanceCheckNotFooledByWhereClause() { let input = """ extension Foo where Self: Bar { public func bar() {} } """ let output = """ public extension Foo where Self: Bar { func bar() {} } """ testFormatting(for: input, output, rule: .extensionAccessControl) } func testAccessNotHoistedIfTypeVisibilityIsLower() { let input = """ class Foo {} extension Foo { public func bar() {} } """ 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) #error("Linux is currently not supported") #endif """ testFormatting(for: input, rule: .extensionAccessControl) } func testExtensionAccessControlRuleTerminatesInFileWithEmptyType() { let input = """ struct Foo { // This type is empty } extension Foo { // This extension is empty } """ testFormatting(for: input, rule: .extensionAccessControl, exclude: [.emptyExtensions]) } }