swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.61k stars 10.37k forks source link

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

Open JosephDuffy opened 1 year ago

JosephDuffy commented 1 year ago

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

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

slavapestov commented 1 year ago

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

DougGregor commented 1 year ago

Also tracked by: rdar://113994346

JosephDuffy commented 1 year ago

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 :)

anton-plebanovich commented 1 year ago

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 commented 1 year ago

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

JosephDuffy commented 12 months ago

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.