@@ -8,14 +8,15 @@ import SwiftSyntaxMacros
88/// A macro that automatically generates an `Equatable` conformance for structs.
99///
1010/// This macro creates a standard equality implementation by comparing all stored properties
11- /// that aren't explicitly marked to be skipped with `@EquatableIgnored`. Properties with SwiftUI property wrappers
12- /// (like `@State`, `@ObservedObject`, etc.)
11+ /// that aren't explicitly marked to be skipped with `@EquatableIgnored`.
12+ /// Properties with SwiftUI property wrappers (like `@State`, `@ObservedObject`, etc.)
1313///
1414/// Structs with arbitary closures are not supported unless they are marked explicitly with `@EquatableIgnoredUnsafeClosure` -
1515/// meaning that they are safe because they don't influence rendering of the view's body.
1616///
1717/// Usage:
1818/// ```swift
19+ /// import Equatable
1920/// import SwiftUI
2021///
2122/// @Equatable
@@ -46,7 +47,33 @@ import SwiftSyntaxMacros
4647/// lhs.id == rhs.id && lhs.username == rhs.username
4748/// }
4849/// }
50+ /// ```
51+ ///
52+ /// If the type is marked as conforming to `Hashable` the compiler synthesized `Hashable` implementation will not be correct.
53+ /// That's why the `@Equatable` macro will also generate a `Hashable` implementation for the type that is aligned with the `Equatable` implementation.
54+ ///
55+ /// ```swift
56+ /// import Equatable
57+ /// @Equatable
58+ /// struct User: Hashable {
59+ /// let id: Int
60+ /// @EquatableIgnored var name = ""
61+ /// }
62+ /// ```
4963///
64+ /// Expanded:
65+ /// ```swift
66+ /// extension User: Equatable {
67+ /// nonisolated public static func == (lhs: User, rhs: User) -> Bool {
68+ /// lhs.id == rhs.id
69+ /// }
70+ /// }
71+ /// extension User {
72+ /// nonisolated public func hash(into hasher: inout Hasher) {
73+ /// hasher.combine(id)
74+ /// }
75+ /// }
76+ /// ```
5077public struct EquatableMacro : ExtensionMacro {
5178 private static let skippablePropertyWrappers : Set = [
5279 " State " ,
@@ -141,25 +168,28 @@ public struct EquatableMacro: ExtensionMacro {
141168 return [ ]
142169 }
143170
144- let comparisons = sortedProperties. map { property in
145- " lhs. \( property. name) == rhs. \( property. name) "
146- } . joined ( separator: " && " )
147-
148- let equalityImplementation = comparisons. isEmpty ? " true " : comparisons
171+ guard let extensionSyntax = Self . generateEquatableExtensionSyntax (
172+ sortedProperties: sortedProperties,
173+ type: type
174+ ) else {
175+ return [ ]
176+ }
149177
150- let extensionDecl : DeclSyntax = """
151- extension \( type) : Equatable {
152- nonisolated public static func == (lhs: \( type) , rhs: \( type) ) -> Bool {
153- \( raw: equalityImplementation)
178+ // Check if the type conforms to `Hashable`
179+ if Self . isHashable ( structDecl) {
180+ // If the type conforms to `Hashable` we need to generate the `Hashable` conformance to match
181+ // the properties used in `Equatable` implementation
182+ guard let hashableExtensionSyntax = Self . generateHashableExtensionSyntax (
183+ sortedProperties: sortedProperties,
184+ type: type
185+ ) else {
186+ return [ extensionSyntax]
154187 }
155- }
156- """
157188
158- guard let extensionSyntax = extensionDecl. as ( ExtensionDeclSyntax . self) else {
159- return [ ]
189+ return [ extensionSyntax, hashableExtensionSyntax]
190+ } else {
191+ return [ extensionSyntax]
160192 }
161-
162- return [ extensionSyntax]
163193 }
164194
165195 // Skip properties with SwiftUI attributes (like @State, @Binding, etc.) or if they are marked with @EqutableIgnored
@@ -209,6 +239,13 @@ public struct EquatableMacro: ExtensionMacro {
209239 }
210240 }
211241
242+ private static func isHashable( _ structDecl: StructDeclSyntax ) -> Bool {
243+ let existingConformances = structDecl. inheritanceClause? . inheritedTypes
244+ . compactMap { $0. type. as ( IdentifierTypeSyntax . self) ? . name. text }
245+ ?? [ ]
246+ return existingConformances. contains ( " Hashable " )
247+ }
248+
212249 private static func isMarkedWithEquatableIgnoredUnsafeClosure( _ varDecl: VariableDeclSyntax ) -> Bool {
213250 varDecl. attributes. contains ( where: { attribute in
214251 if let attributeName = attribute. as ( AttributeSyntax . self) ? . attributeName. as ( IdentifierTypeSyntax . self) ? . name. text {
@@ -288,7 +325,7 @@ public struct EquatableMacro: ExtensionMacro {
288325 Consider marking the closure with \
289326 @EquatableIgnoredUnsafeClosure if it doesn't effect the view's body output.
290327 """ ,
291- fixItID: . init (
328+ fixItID: MessageID (
292329 domain: " " ,
293330 id: " test "
294331 )
@@ -300,104 +337,54 @@ public struct EquatableMacro: ExtensionMacro {
300337
301338 return diagnostic
302339 }
303- }
304340
305- @main
306- struct EquatablePlugin : CompilerPlugin {
307- let providingMacros : [ Macro . Type ] = [
308- EquatableMacro . self,
309- EquatableIgnoredMacro . self,
310- EquatableIgnoredUnsafeClosureMacro . self
311- ]
312- }
313-
314- struct SimpleFixItMessage : FixItMessage {
315- let message : String
316- let fixItID : MessageID
317- }
318-
319- extension TypeSyntax {
320- var isSwift : Bool {
321- if self . as ( IdentifierTypeSyntax . self) ? . isSwift ?? false {
322- return true
323- }
324- return false
325- }
326- }
341+ private static func generateEquatableExtensionSyntax(
342+ sortedProperties: [ ( name: String , type: TypeSyntax ? ) ] ,
343+ type: TypeSyntaxProtocol
344+ ) -> ExtensionDeclSyntax ? {
345+ let comparisons = sortedProperties. map { property in
346+ " lhs. \( property. name) == rhs. \( property. name) "
347+ } . joined ( separator: " && " )
327348
328- extension TypeSyntax {
329- var isArray : Bool {
330- if self . is ( ArrayTypeSyntax . self) {
331- return true
332- }
333- if self . as ( IdentifierTypeSyntax . self) ? . isArray ?? false {
334- return true
335- }
336- if self . as ( MemberTypeSyntax . self) ? . isArray ?? false {
337- return true
338- }
339- return false
340- }
341- }
349+ let equalityImplementation = comparisons. isEmpty ? " true " : comparisons
342350
343- extension IdentifierTypeSyntax {
344- var isSwift : Bool {
345- if self . name. text == " Swift " {
346- return true
351+ let extensionDecl : DeclSyntax = """
352+ extension \( type) : Equatable {
353+ nonisolated public static func == (lhs: \( type) , rhs: \( type) ) -> Bool {
354+ \( raw: equalityImplementation)
355+ }
347356 }
348- return false
349- }
350- }
357+ """
351358
352- extension IdentifierTypeSyntax {
353- var isArray : Bool {
354- if self . name. text == " Array " {
355- return true
356- }
357- return false
359+ return extensionDecl. as ( ExtensionDeclSyntax . self)
358360 }
359- }
360361
361- extension IdentifierTypeSyntax {
362- var isDictionary : Bool {
363- if self . name. text == " Dictionary " {
364- return true
362+ private static func generateHashableExtensionSyntax(
363+ sortedProperties: [ ( name: String , type: TypeSyntax ? ) ] ,
364+ type: TypeSyntaxProtocol
365+ ) -> ExtensionDeclSyntax ? {
366+ let hashableImplementation = sortedProperties. map { property in
367+ " hasher.combine( \( property. name) ) "
365368 }
366- return false
367- }
368- }
369+ . joined ( separator: " \n " )
369370
370- extension MemberTypeSyntax {
371- var isArray : Bool {
372- if self . baseType . isSwift ,
373- self . name . text == " Array " {
374- return true
371+ let hashableExtensionDecl : DeclSyntax = """
372+ extension \( raw : type ) {
373+ nonisolated public func hash(into hasher: inout Hasher) {
374+ \( raw : hashableImplementation )
375+ }
375376 }
376- return false
377- }
378- }
377+ """
379378
380- extension MemberTypeSyntax {
381- var isDictionary : Bool {
382- if self . baseType. isSwift,
383- self . name. text == " Dictionary " {
384- return true
385- }
386- return false
379+ return hashableExtensionDecl. as ( ExtensionDeclSyntax . self)
387380 }
388381}
389382
390- extension TypeSyntax {
391- var isDictionary : Bool {
392- if self . is ( DictionaryTypeSyntax . self) {
393- return true
394- }
395- if self . as ( IdentifierTypeSyntax . self) ? . isDictionary ?? false {
396- return true
397- }
398- if self . as ( MemberTypeSyntax . self) ? . isDictionary ?? false {
399- return true
400- }
401- return false
402- }
383+ @main
384+ struct EquatablePlugin : CompilerPlugin {
385+ let providingMacros : [ Macro . Type ] = [
386+ EquatableMacro . self,
387+ EquatableIgnoredMacro . self,
388+ EquatableIgnoredUnsafeClosureMacro . self
389+ ]
403390}
0 commit comments