From 147d0d6019bee8acbb07f86981100ab37ae2fb64 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:44:38 -0800 Subject: [PATCH] Update `redundantEquatable` to preserve `==` implementation in types with `Strideable` conformance (#2417) Co-authored-by: calda <1811727+calda@users.noreply.github.com> --- README.md | 2 + Sources/Rules/RedundantEquatable.swift | 10 +++ Tests/Rules/RedundantEquatableTests.swift | 104 ++++++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/README.md b/README.md index 82cf65d2..9ee0a04e 100644 --- a/README.md +++ b/README.md @@ -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 ----------- diff --git a/Sources/Rules/RedundantEquatable.swift b/Sources/Rules/RedundantEquatable.swift index 84b4e59d..6adfe407 100644 --- a/Sources/Rules/RedundantEquatable.swift +++ b/Sources/Rules/RedundantEquatable.swift @@ -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 = [] 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 } diff --git a/Tests/Rules/RedundantEquatableTests.swift b/Tests/Rules/RedundantEquatableTests.swift index c3162475..28de5fb9 100644 --- a/Tests/Rules/RedundantEquatableTests.swift +++ b/Tests/Rules/RedundantEquatableTests.swift @@ -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) + } }