Add redundantSendable rule (#2394)

This commit is contained in:
NachoSoto
2026-02-20 13:35:03 -08:00
committed by GitHub
parent cf946767a0
commit 3d3f0503c5
4 changed files with 247 additions and 0 deletions
+24
View File
@@ -69,6 +69,7 @@
* [redundantRawValues](#redundantRawValues)
* [redundantReturn](#redundantReturn)
* [redundantSelf](#redundantSelf)
* [redundantSendable](#redundantSendable)
* [redundantStaticSelf](#redundantStaticSelf)
* [redundantSwiftTestingSuite](#redundantSwiftTestingSuite)
* [redundantThrows](#redundantThrows)
@@ -2929,6 +2930,29 @@ by using `--self init-only`:
</details>
<br/>
## redundantSendable
Remove redundant explicit Sendable conformance from non-public structs and enums.
<details>
<summary>Examples</summary>
```diff
- struct CacheEntry: Sendable {
+ struct CacheEntry {
let id: String
}
- fileprivate enum ParsingState: Sendable {
+ fileprivate enum ParsingState {
case idle
case running
}
```
</details>
<br/>
## redundantStaticSelf
Remove explicit `Self` where applicable.
+1
View File
@@ -92,6 +92,7 @@ let ruleRegistry: [String: FormatRule] = [
"redundantRawValues": .redundantRawValues,
"redundantReturn": .redundantReturn,
"redundantSelf": .redundantSelf,
"redundantSendable": .redundantSendable,
"redundantStaticSelf": .redundantStaticSelf,
"redundantSwiftTestingSuite": .redundantSwiftTestingSuite,
"redundantThrows": .redundantThrows,
+103
View File
@@ -0,0 +1,103 @@
//
// RedundantSendable.swift
// SwiftFormat
//
// Created by Nacho Soto on 2/20/2026.
//
import Foundation
public extension FormatRule {
static let redundantSendable = FormatRule(
help: "Remove redundant explicit Sendable conformance from non-public structs and enums."
) { formatter in
let declarations = formatter.parseDeclarations()
declarations.forEachRecursiveDeclaration { declaration in
guard let typeDeclaration = declaration.asTypeDeclaration,
typeDeclaration.keyword == "struct" || typeDeclaration.keyword == "enum"
else { return }
switch typeDeclaration.visibility() {
case .public, .open:
return
case .internal, .package, .fileprivate, .private, nil:
break
}
guard let sendableConformance = typeDeclaration.conformances.first(where: {
formatter.isRedundantSendableConformance($0.conformance)
}) else { return }
formatter.removeConformance(
at: sendableConformance.index,
range: sendableConformance.conformance.range
)
}
} examples: {
"""
```diff
- struct CacheEntry: Sendable {
+ struct CacheEntry {
let id: String
}
- fileprivate enum ParsingState: Sendable {
+ fileprivate enum ParsingState {
case idle
case running
}
```
"""
}
}
extension Formatter {
func isRedundantSendableConformance(_ conformance: TypeName) -> Bool {
let significantTokens = conformance.tokens.filter { !$0.isSpaceOrCommentOrLinebreak }
guard !significantTokens.contains(where: { $0.isAttribute && $0.string == "@unchecked" }) else {
return false
}
if significantTokens == [.identifier("Sendable")] {
return true
}
guard significantTokens.count == 3,
significantTokens[0] == .identifier("Swift"),
significantTokens[2] == .identifier("Sendable")
else {
return false
}
let dotToken = significantTokens[1]
return dotToken.isOperator(".") || dotToken == .delimiter(".")
}
func removeConformance(at conformanceIndex: Int, range conformanceRange: ClosedRange<Int>) {
guard let previousTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: conformanceIndex),
let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: conformanceRange.upperBound)
else { return }
let removalRange: ClosedRange<Int>
if tokens[nextTokenIndex] == .delimiter(",") {
let upperBound: Int
if token(at: nextTokenIndex + 1)?.isSpace == true {
upperBound = nextTokenIndex + 1
} else {
upperBound = nextTokenIndex
}
removalRange = conformanceIndex ... upperBound
} else {
removalRange = previousTokenIndex ... conformanceRange.upperBound
}
// Avoid removing inline comments attached to the conformance list.
guard !tokens[removalRange].contains(where: \.isComment) else {
return
}
removeTokens(in: removalRange)
}
}
+119
View File
@@ -0,0 +1,119 @@
//
// RedundantSendableTests.swift
// SwiftFormatTests
//
// Created by Nacho Soto on 2/20/2026.
//
import XCTest
@testable import SwiftFormat
final class RedundantSendableTests: XCTestCase {
func testRemovesSendableFromNestedAndTopLevelNonPublicValueTypes() {
let input = """
struct Outer {
enum NestedImplicitAccess: Sendable {}
private struct NestedPrivate: Sendable {}
@available(*, deprecated)
enum NestedAttributed: Sendable {}
}
struct TopLevelImplicitAccess: Sendable {}
fileprivate enum TopLevelFileprivate: Sendable {}
"""
let output = """
struct Outer {
enum NestedImplicitAccess {}
private struct NestedPrivate {}
@available(*, deprecated)
enum NestedAttributed {}
}
struct TopLevelImplicitAccess {}
fileprivate enum TopLevelFileprivate {}
"""
testFormatting(
for: input,
[output],
rules: [.redundantSendable],
exclude: [.enumNamespaces, .redundantFileprivate]
)
}
func testDoesNotRemoveSendableFromValidCases() {
let input = """
public struct PublicValue: Sendable {}
public enum PublicEnum: Sendable {}
private final class PrivateReference: Sendable {}
private struct PrivateUnchecked: @unchecked Sendable {}
struct NoExplicitSendable {}
struct Generic<T>: Equatable where T: Sendable {}
"""
testFormatting(for: input, rules: [.redundantSendable], exclude: [.simplifyGenericConstraints])
}
func testIgnoresCommentsAndStrings() {
let input = """
func demo() {
let example = \"\"\"
enum FakeNested: Sendable {}
private struct AlsoFake: Sendable {}
\"\"\"
_ = example
// struct CommentFake: Sendable {}
/*
fileprivate enum BlockCommentFake: Sendable {}
*/
}
"""
testFormatting(for: input, rules: [.redundantSendable], exclude: [.indent])
}
func testRemovesQualifiedSendableFromMixedConformanceLists() {
let input = """
struct First: Sendable, Codable {}
struct Middle: Codable, Swift.Sendable, Hashable {}
enum Last: CaseIterable, Sendable {
case value
}
"""
let output = """
struct First: Codable {}
struct Middle: Codable, Hashable {}
enum Last: CaseIterable {
case value
}
"""
testFormatting(for: input, [output], rules: [.redundantSendable])
}
func testRemovesSendableFromPackageValueTypes() {
let input = """
package struct PackageStruct: Sendable {
package let value: Int
}
package enum PackageEnum: Sendable {
case value(Int)
}
"""
let output = """
package struct PackageStruct {
package let value: Int
}
package enum PackageEnum {
case value(Int)
}
"""
testFormatting(for: input, [output], rules: [.redundantSendable])
}
}