swift icon indicating copy to clipboard operation
swift copied to clipboard

`==` implementation added in a macro is not considered when checking for `Equatable` conformance

Open JosephDuffy opened this issue 1 year ago • 1 comments

Description

Writing a macro that outputs a == function is not considered when checking for Equatable conformance.

Steps to reproduce

Write a macro conforming to ConformanceMacro and MemberMacro. Add Hashable conformance via the expansion(of:providingConformancesOf:in:) function and a valid static func == implementation via expansion(of:providingMembersOf:in:).

Expected behavior

The compiler should use the == function produced by the macro.

Screenshots

Screenshot 2023-06-05 at 23 41 43 Screenshot 2023-06-05 at 23 41 58

Environment

  • swift-driver version: 1.82.2 Apple Swift version 5.9 (swiftlang-5.9.0.114.6 clang-1500.0.27.1)
  • Xcode 15.0 (Build version 15A5160n)
  • Deployment target:
    • .macOS(.v10_15)
    • .iOS(.v13)
    • .tvOS(.v13)
    • .watchOS(.v6)
    • .macCatalyst(.v13)

Additional context

As shown in the screenshots the generated code looks right. Copy/pasting the generated code results in an "Invalid redeclaration of ==" error, then removing the == from the macro output allows the code to compile and the associated tests to pass, so I'm concluding (I hope correctly) that my implementation is correct!

It's also possible to workaround this issue by conforming to a custom protocol that adds a == function via extension.

I have an example of the workaround being used in a repo: https://github.com/JosephDuffy/CustomHashable. The remove-customHashable branch can be used to demonstrate the bug.

A topic I created on the Swift Forums a few weeks back for this: https://forums.swift.org/t/creating-a-macro-for-hashable-equatable-conformance/64711

JosephDuffy avatar Jun 05 '23 22:06 JosephDuffy

I believe this might be related to https://github.com/apple/swift/pull/66320. CC @DougGregor

slavapestov avatar Jun 06 '23 17:06 slavapestov

Also tracked by: rdar://113994346

DougGregor avatar Aug 17 '23 04:08 DougGregor

I've tested this using Xcode 15 beta 6 and beta 7 and have found it works for me, thank you for getting this fixed!

I'm not sure if there's some extra process here but I'm happy to close this issue if you are :)

JosephDuffy avatar Sep 05 '23 20:09 JosephDuffy

Probably related. I tried both solutions but when I tried to add the == function for a child of NSObject it did not work. It uses the default NSObject implementation as I understand. It properly shows synthesized code when I expand the macro. The same lines entered manually work.

Breakpoint never fired also image

// main.swift
@MyEquatable
class MyClass: NSObject {
    let string: String = ""
}

let asd1 = MyClass()
let asd2 = MyClass()
print(asd1 == asd1) // true
print(asd1 == asd2) // false
// MyEquatableMacro.swift
public struct MyEquatable: MemberMacro {
    public static func expansion(
        of node: SwiftSyntax.AttributeSyntax,
        providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
        in context: some SwiftSyntaxMacros.MacroExpansionContext
    ) throws -> [SwiftSyntax.DeclSyntax] {
        guard let classDeclSyntax = declaration.as(ClassDeclSyntax.self) else {
            return []
        }
        
        let className = classDeclSyntax.name
        
        let memberList = classDeclSyntax.memberBlock.members
        let variableDecls = memberList.compactMap { $0.decl.as(VariableDeclSyntax.self) }
        let identifierPatterns = variableDecls.compactMap { $0.bindings.first?.pattern.as(IdentifierPatternSyntax.self) }
        let variableIdentifiers = identifierPatterns.map { $0.identifier }
        let leadingTrivia = variableDecls.first?.leadingTrivia ?? Trivia(pieces: [])
        
        let function = try FunctionDeclSyntax("static func ==(lhs: \(className), rhs: \(className)) -> Bool") {
            for (index, variableIdentifier) in variableIdentifiers.enumerated() {
                if index == 0 {
                    "lhs.\(variableIdentifier) == rhs.\(variableIdentifier)"
                } else {
                    "\(leadingTrivia)&& lhs.\(variableIdentifier) == rhs.\(variableIdentifier)"
                }
            }
        }
        
        return [DeclSyntax(function)]
    }
}

anton-plebanovich avatar Nov 17 '23 19:11 anton-plebanovich

I made a workaround with the isEqual override but the visibility of the == is still a bug

anton-plebanovich avatar Nov 18 '23 08:11 anton-plebanovich

Testing this in the Xcode 15.0.1 release I see the issue again, but in Xcode 15.1 beta 2 it looks to be fixed again.

I can't see a way to prevent a package from being used in Xcode 15.0.1 but allow it in Xcode 15.1 though. Both swift-tools-version: 5.9.2 and swiftLanguageVersions: [.version("5.9.2")] prevent the package being used in Xcode 15.1 beta 2.

JosephDuffy avatar Nov 21 '23 03:11 JosephDuffy