mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
Add redundantSendable rule (#2394)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -92,6 +92,7 @@ let ruleRegistry: [String: FormatRule] = [
|
||||
"redundantRawValues": .redundantRawValues,
|
||||
"redundantReturn": .redundantReturn,
|
||||
"redundantSelf": .redundantSelf,
|
||||
"redundantSendable": .redundantSendable,
|
||||
"redundantStaticSelf": .redundantStaticSelf,
|
||||
"redundantSwiftTestingSuite": .redundantSwiftTestingSuite,
|
||||
"redundantThrows": .redundantThrows,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user