mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
fd5b4ac79c
Co-authored-by: calda <1811727+calda@users.noreply.github.com>
127 lines
4.4 KiB
Swift
127 lines
4.4 KiB
Swift
//
|
|
// 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 }
|
|
|
|
// Indirect enums with recursive cases are not implicitly Sendable,
|
|
// so we must not remove an explicit Sendable conformance from them.
|
|
if typeDeclaration.keyword == "enum", typeDeclaration.hasModifier("indirect") {
|
|
return
|
|
}
|
|
|
|
switch typeDeclaration.visibility() {
|
|
case .public, .open:
|
|
return
|
|
case .internal, .package, .fileprivate, .private:
|
|
break
|
|
case nil:
|
|
// A type with no explicit access modifier inside a public extension is effectively public
|
|
let isInPublicExtension = typeDeclaration.parentDeclarations.last.map {
|
|
$0.keyword == "extension" && $0.visibility() == .public
|
|
} ?? false
|
|
if isInPublicExtension {
|
|
return
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// When removing all conformances, also strip any space tokens immediately
|
|
// before the colon to avoid leaving a trailing double space
|
|
// (e.g. `enum Bar : Sendable {` → `enum Bar {`, not `enum Bar {`).
|
|
var lower = previousTokenIndex
|
|
if tokens[lower] == .delimiter(":") {
|
|
while lower > 0, token(at: lower - 1)?.isSpace == true {
|
|
lower -= 1
|
|
}
|
|
}
|
|
removalRange = lower ... conformanceRange.upperBound
|
|
}
|
|
|
|
// Avoid removing inline comments attached to the conformance list.
|
|
guard !tokens[removalRange].contains(where: \.isComment) else {
|
|
return
|
|
}
|
|
|
|
removeTokens(in: removalRange)
|
|
}
|
|
}
|