Update redundantEquatable to preserve == implementation in types with Strideable conformance (#2417)

Co-authored-by: calda <1811727+calda@users.noreply.github.com>
This commit is contained in:
Copilot
2026-02-26 09:17:43 +00:00
committed by Nick Lockwood
co-authored by calda
parent d6932a9c52
commit 147d0d6019
3 changed files with 116 additions and 0 deletions
+2
View File
@@ -1086,6 +1086,8 @@ Known issues
* The `propertyTypes` rule can cause a build failure in cases like `let foo = Foo.bar` where the value is a static member that doesn't return the same time. For example, `let foo: Foo = .bar` would be invalid if the `bar` property was defined as `static var bar: Bar`. As a workaround you can write the name of the type explicitly, like `let foo: Bar = Foo.bar`, or exclude the type name and/or property name with `--preserve-symbols Bar,bar,etc`.
* The `redundantEquatable` rule may incorrectly remove a `==` implementation that intentionally overrides a default `==` provided by another protocol. For example, `Strideable` provides a default `==` implementation derived from `distance(to:)`, and a type may override it with a custom `==`. SwiftFormat handles this case when the `Strideable` conformance is defined in the same file, but it cannot detect conformances in a different file, nor default `==` implementations provided by other protocols. As a workaround you can use the `// swiftformat:disable:next redundantEquatable` comment directive to disable the rule for the affected type (or just disable the `redundantEquatable` rule completely).
Tip Jar
-----------
+10
View File
@@ -153,6 +153,7 @@ extension Formatter {
func manuallyImplementedEquatableTypes(in declarations: [Declaration]) -> [EquatableType] {
var typeDeclarationsByFullyQualifiedName: [String: Declaration] = [:]
var typesWithEquatableConformances: [(fullyQualifiedTypeName: String, declarationWithEquatableConformance: Declaration)] = []
var typesWithStrideableConformances: Set<String> = []
var equatableImplementationsByFullyQualifiedName: [String: Declaration] = [:]
declarations.forEachRecursiveDeclaration { declaration in
@@ -178,6 +179,11 @@ extension Formatter {
declarationWithEquatableConformance: declaration
))
}
// Strideable provides a default `==` implementation, so a custom `==` may not be redundant
if conformances.contains(where: { $0.conformance.string == "Strideable" }) {
typesWithStrideableConformances.insert(fullyQualifiedName)
}
}
if declaration.keyword == "func",
@@ -229,6 +235,10 @@ extension Formatter {
}
return typesWithEquatableConformances.compactMap { typeName, declarationWithEquatableConformance in
// Types conforming to Strideable get a default `==` implementation via that protocol,
// so a custom `==` on such a type may be intentionally overriding that default.
guard !typesWithStrideableConformances.contains(typeName) else { return nil }
guard let typeDeclaration = typeDeclarationsByFullyQualifiedName[typeName],
let equatableImplementation = equatableImplementationsByFullyQualifiedName[typeName]
else { return nil }
+104
View File
@@ -649,4 +649,108 @@ final class RedundantEquatableTests: XCTestCase {
testFormatting(for: input, rule: .redundantEquatable)
}
func testPreserveEquatableImplementationForStrideableType() {
// `Strideable` provides a default `==` implementation via `distance(to:)`,
// so a custom `==` on a Strideable type may be intentionally overriding that default.
let input = """
struct Foo: Strideable, Equatable {
let value: Int
static func == (lhs: Foo, rhs: Foo) -> Bool {
lhs.value == rhs.value
}
func distance(to other: Foo) -> Int {
other.value - value
}
func advanced(by n: Int) -> Foo {
Foo(value: value + n)
}
}
"""
testFormatting(for: input, rule: .redundantEquatable)
}
func testPreserveEquatableImplementationForStrideableTypeInExtension() {
// `Strideable` provides a default `==` implementation via `distance(to:)`,
// so a custom `==` on a Strideable type may be intentionally overriding that default.
let input = """
struct Foo {
let value: Int
}
extension Foo: Strideable {
func distance(to other: Foo) -> Int {
other.value - value
}
func advanced(by n: Int) -> Foo {
Foo(value: value + n)
}
}
extension Foo: Equatable {
static func == (lhs: Foo, rhs: Foo) -> Bool {
lhs.value == rhs.value
}
}
"""
testFormatting(for: input, rule: .redundantEquatable)
}
func testPreserveEquatableImplementationForStrideableTypeWithEquatableInExtension() {
// `Strideable` provides a default `==` implementation via `distance(to:)`,
// so a custom `==` on a Strideable type may be intentionally overriding that default.
let input = """
struct Foo: Strideable {
let value: Int
func distance(to other: Foo) -> Int {
other.value - value
}
func advanced(by n: Int) -> Foo {
Foo(value: value + n)
}
}
extension Foo: Equatable {
static func == (lhs: Foo, rhs: Foo) -> Bool {
lhs.value == rhs.value
}
}
"""
testFormatting(for: input, rule: .redundantEquatable)
}
func testPreserveEquatableImplementationForStrideableTypeWithStrideableInExtension() {
// `Strideable` provides a default `==` implementation via `distance(to:)`,
// so a custom `==` on a Strideable type may be intentionally overriding that default.
let input = """
struct Foo: Equatable {
let value: Int
static func == (lhs: Foo, rhs: Foo) -> Bool {
lhs.value == rhs.value
}
}
extension Foo: Strideable {
func distance(to other: Foo) -> Int {
other.value - value
}
func advanced(by n: Int) -> Foo {
Foo(value: value + n)
}
}
"""
testFormatting(for: input, rule: .redundantEquatable)
}
}