diff --git a/Sources/ParsingHelpers.swift b/Sources/ParsingHelpers.swift index 8b022027..115a02ca 100644 --- a/Sources/ParsingHelpers.swift +++ b/Sources/ParsingHelpers.swift @@ -1014,6 +1014,17 @@ extension Formatter { } } + /// The opening tokens of the declaration (before the body) + var openTokens: [Token] { + switch self { + case .declaration: + return [] + case let .type(_, open, _, _), + let .conditionalCompilation(open, _, _): + return open + } + } + /// The body of this declaration, if applicable var body: [Declaration]? { switch self { diff --git a/Sources/Rules.swift b/Sources/Rules.swift index df10e240..4d57c7be 100644 --- a/Sources/Rules.swift +++ b/Sources/Rules.swift @@ -4973,6 +4973,14 @@ public struct _FormatRules { // remove the keyword from the individual declarations and // place it on the extension itself. case .onExtension: + if extensionVisibility == nil, + let delimiterIndex = declaration.openTokens.index(of: .delimiter(":")), + declaration.openTokens.index(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 diff --git a/Tests/RulesTests+Organization.swift b/Tests/RulesTests+Organization.swift index b592ef18..6e5a57da 100644 --- a/Tests/RulesTests+Organization.swift +++ b/Tests/RulesTests+Organization.swift @@ -1503,4 +1503,27 @@ extension RulesTests { """ testFormatting(for: input, rule: FormatRules.extensionAccessControl) } + + func testNoHoistAccessModifierForExtensionThatAddsProtocolConformance() { + let input = """ + extension Foo: Bar { + public func bar() {} + } + """ + testFormatting(for: input, rule: FormatRules.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: FormatRules.extensionAccessControl) + } } diff --git a/Tests/XCTestManifests.swift b/Tests/XCTestManifests.swift index 595b80a4..2325f750 100644 --- a/Tests/XCTestManifests.swift +++ b/Tests/XCTestManifests.swift @@ -1077,6 +1077,7 @@ extension RulesTests { ("testNoExtraSpaceInsideMultilineHeaderdocCommentType2", testNoExtraSpaceInsideMultilineHeaderdocCommentType2), ("testNoExtraSpaceInsideMultilineSwiftPlaygroundDocComment", testNoExtraSpaceInsideMultilineSwiftPlaygroundDocComment), ("testNoFractionGrouping", testNoFractionGrouping), + ("testNoHoistAccessModifierForExtensionThatAddsProtocolConformance", testNoHoistAccessModifierForExtensionThatAddsProtocolConformance), ("testNoHoistAccessModifierForOpenMethod", testNoHoistAccessModifierForOpenMethod), ("testNoHoistClosureVariables", testNoHoistClosureVariables), ("testNoHoistIfFirstArgSpecified", testNoHoistIfFirstArgSpecified), @@ -1489,6 +1490,7 @@ extension RulesTests { ("testPrivateRequiredStaticFuncModifiers", testPrivateRequiredStaticFuncModifiers), ("testPrivateSetModifierNotMangled", testPrivateSetModifierNotMangled), ("testPropertyTypeNotConvertedToSugar", testPropertyTypeNotConvertedToSugar), + ("testProtocolConformanceCheckNotFooledByWhereClause", testProtocolConformanceCheckNotFooledByWhereClause), ("testPublicExtensionMemberACLStripped", testPublicExtensionMemberACLStripped), ("testPublicInitCoderUnavailable", testPublicInitCoderUnavailable), ("testPublicInitCoderUnavailable2", testPublicInitCoderUnavailable2),