diff --git a/Sources/Rules/RedundantViewBuilder.swift b/Sources/Rules/RedundantViewBuilder.swift index ad12b010..b6e4f73e 100644 --- a/Sources/Rules/RedundantViewBuilder.swift +++ b/Sources/Rules/RedundantViewBuilder.swift @@ -20,6 +20,12 @@ public extension FormatRule { guard let viewBuilderIndex = formatter.indexOfViewBuilderAttribute(for: declaration) else { return } + // Never remove @ViewBuilder from protocol members, as conforming types + // rely on the implicit result builder being added + if formatter.isInsideProtocol(at: declaration.keywordIndex) { + return + } + let bodyScope: ClosedRange? let isBodyMember: Bool diff --git a/Tests/Rules/RedundantViewBuilderTests.swift b/Tests/Rules/RedundantViewBuilderTests.swift index bc5090f0..7a079bfa 100644 --- a/Tests/Rules/RedundantViewBuilderTests.swift +++ b/Tests/Rules/RedundantViewBuilderTests.swift @@ -497,4 +497,45 @@ final class RedundantViewBuilderTests: XCTestCase { """ testFormatting(for: input, output, rule: .redundantViewBuilder) } + + func testKeepViewBuilderOnProtocolMember() { + // Protocol members with @ViewBuilder should not have it removed, + // as conforming types rely on the implicit result builder + let input = """ + protocol Foo { + associatedtype MyFoo: View + + @ViewBuilder + var myBody: MyFoo { get } + } + """ + testFormatting(for: input, rule: .redundantViewBuilder) + } + + func testKeepViewBuilderOnProtocolFunction() { + let input = """ + protocol ViewProvider { + @ViewBuilder + func makeView() -> some View + } + """ + testFormatting(for: input, rule: .redundantViewBuilder) + } + + func testKeepViewBuilderOnProtocolComputedProperty() { + let input = """ + protocol ContentProvider { + @ViewBuilder + var content: some View { get } + } + + struct MyContent: ContentProvider { + var content: some View { + Text("Hello") + Text("World") + } + } + """ + testFormatting(for: input, rule: .redundantViewBuilder) + } }