Skip to content

Commit bc210ec

Browse files
committed
Add @ConformToHashable
Minor format adjust
1 parent d5213ca commit bc210ec

File tree

7 files changed

+200
-6
lines changed

7 files changed

+200
-6
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ A practical collection of Swift Macros that help code correctly and swiftly.
2424
| |<pre>let url = #buildURL("http://google.com",<br> URLScheme.https,<br> URLQueryItems([.init(name: "q1", value: "q1v"), .init(name: "q2", value: "q2v")]))<br>let url2 = buildURL {<br> "http://google.com"<br> URLScheme.https<br> URLQueryItems([.init(name: "q1", value: "q1v"), .init(name: "q2", value: "q2v")])<br>}</pre>|
2525
| #buildURLRequest |Build a URLRequest from components.<br>This solution addes in a resultBulder `URLRequestBuilder`, which can be used directly if prefer builder pattern. |
2626
| |<pre>let urlrequest = #buildURLRequest(url!, RequestTimeOutInterval(100))<br>let urlRequest2 = buildURLRequest {<br> url!<br> RequestTimeOutInterval(100)<br>}</pre>|
27-
| @ConformToEquatable|Add Equtable conformance to a class type<br>Use it caution per https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#synthesis-for-class-types-and-tuples|
27+
| @ConformToEquatable|Add Equtable conformance to a class type<br>Use it with caution per https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#synthesis-for-class-types-and-tuples|
2828
| |<pre>@AddInit<br>@ConformToEquatable<br>class AClass {<br> let a: Int?<br> let b: () -> Void<br>}</pre>|
29+
| @ConformToHashable|Add Hashable conformance to a class type<br>Use it with caution per https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#synthesis-for-class-types-and-tuples|
30+
| |<pre>@AddInit<br>@ConformToHashable<br>class AClass {<br> let a: Int?<br> let b: () -> Void<br>}</pre>|
2931
| #encode |Encode an Encodable to data using JSONEncoder |
3032
| |<pre>#encode(value)</pre>|
3133
| #decode |Decode a Decodable to a typed value using JSONDecoder |

Sources/Client/main.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@ struct TestAccess {
147147
var keychainValue: TestStruct?
148148
}
149149

150-
151150
@AddInit
152151
@ConformToEquatable
152+
@ConformToHashable
153153
class AClass {
154154
let a: Int?
155155
let b: () -> Void
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxBuilder
3+
import SwiftSyntaxMacros
4+
import Foundation
5+
6+
public struct ConformToHashable: ExtensionMacro {
7+
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
8+
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
9+
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
10+
conformingTo protocols: [SwiftSyntax.TypeSyntax],
11+
in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
12+
guard [SwiftSyntax.SyntaxKind.classDecl].contains(declaration.kind) else {
13+
throw MacroDiagnostics.errorMacroUsage(message: "Can only be applied to a class type")
14+
}
15+
let equatableProtocol = InheritanceClauseSyntax(inheritedTypes: InheritedTypeListSyntax(
16+
arrayLiteral: InheritedTypeSyntax(type: TypeSyntax(stringLiteral: "Hashable")))
17+
)
18+
19+
let mambers = declaration.memberBlock.members.compactMap { member in
20+
if let patternBinding = member.decl.as(VariableDeclSyntax.self)?.bindings
21+
.as(PatternBindingListSyntax.self)?.first?.as(PatternBindingSyntax.self),
22+
let identifier = patternBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
23+
let type = patternBinding.typeAnnotation?.as(TypeAnnotationSyntax.self)?.type {
24+
if type.is(IdentifierTypeSyntax.self) {
25+
return "hasher.combine(\(identifier))"
26+
}
27+
if let wrappedType = type.as(OptionalTypeSyntax.self)?.wrappedType,
28+
wrappedType.is(IdentifierTypeSyntax.self) {
29+
return "hasher.combine(\(identifier))"
30+
}
31+
}
32+
return nil
33+
}.joined(separator: "\n ")
34+
35+
let equtableFunction = """
36+
func hash(into hasher: inout Hasher) {
37+
\(mambers)
38+
}
39+
"""
40+
41+
let member = MemberBlockSyntax(members: MemberBlockItemListSyntax(stringLiteral: equtableFunction))
42+
let extensionDecl = ExtensionDeclSyntax(extendedType: type,
43+
inheritanceClause: equatableProtocol,
44+
memberBlock: member)
45+
return [extensionDecl]
46+
}
47+
}

Sources/Macros/MacroPlugin.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct MacroPlugin: CompilerPlugin {
1919
Mock.self,
2020
PostNotification.self,
2121
Singleton.self,
22-
ConformToEquatable.self
22+
ConformToEquatable.self,
23+
ConformToHashable.self
2324
]
2425
}

Sources/SwiftMacros/SwiftMacros.docc/SwiftMacros.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This collection of Swift Macros aims to remove boilerplate code by automatically
2222
- ``buildDate(_:)``
2323
- ``buildURL(_:)``
2424
- ``buildURLRequest(_:)``
25+
- ``ConformToEqutable``
26+
- ``ConformToHashable``
2527
- ``encode(_:outputFormatting:dateEncodingStrategy:dataEncodingStrategy:nonConformingFloatEncodingStrategy:keyEncodingStrategy:userInfo:)``
2628
- ``decode(_:from:dateDecodingStrategy:dataDecodingStrategy:nonConformingFloatDecodingStrategy:keyDecodingStrategy:userInfo:allowsJSON5:assumesTopLevelDictionary:)``
2729
- ``formatDate(_:dateStyle:timeStyle:formattingContext:formatterBehavior:doesRelativeDateFormatting:amSymbol:pmSymbol:weekdaySymbols:shortWeekdaySymbols:veryShortWeekdaySymbols:standaloneWeekdaySymbols:shortStandaloneWeekdaySymbols:veryShortStandaloneWeekdaySymbols:monthSymbols:shortMonthSymbols:veryShortMonthSymbols:standaloneMonthSymbols:shortStandaloneMonthSymbols:veryShortStandaloneMonthSymbols:quarterSymbols:shortQuarterSymbols:standaloneQuarterSymbols:shortStandaloneQuarterSymbols:eraSymbols:longEraSymbols:)``
@@ -32,5 +34,4 @@ This collection of Swift Macros aims to remove boilerplate code by automatically
3234
- ``Mock(type:randomMockValue:)``
3335
- ``postNotification(_:object:userInfo:from:)``
3436
- ``Singleton``
35-
- ``ConformToEqutable``
3637

Sources/SwiftMacros/SwiftMacros.swift

+37-2
Original file line numberDiff line numberDiff line change
@@ -607,13 +607,48 @@ public macro Singleton() = #externalMacro(module: "Macros", type: "Singleton")
607607
/// ```
608608
/// will expand to
609609
/// ```swift
610-
/// class A: Equtable {
610+
/// class A {
611611
/// let a: Int
612612
/// init(a: Int) {
613613
/// self.a = a
614614
/// }
615-
///
615+
/// }
616+
///
617+
/// extension A: Equatable {
618+
/// static func == (lhs: A, rhs: A) -> Bool {
619+
/// lhs.a == rhs.a
620+
/// }
616621
/// }
617622
/// ```
618623
@attached(extension, conformances: Equatable, names: named(==))
619624
public macro ConformToEquatable() = #externalMacro(module: "Macros", type: "ConformToEquatable")
625+
626+
/// Add Hashable conformance to class
627+
///
628+
/// For example:
629+
/// ```swift
630+
/// @ConformToHashable
631+
/// class A {
632+
/// let a: Int
633+
/// init(a: Int) {
634+
/// self.a = a
635+
/// }
636+
/// }
637+
/// ```
638+
/// will expand to
639+
/// ```swift
640+
/// class A {
641+
/// let a: Int
642+
/// init(a: Int) {
643+
/// self.a = a
644+
/// }
645+
/// }
646+
///
647+
/// extension A: Hashable {
648+
/// func hash(into hasher: inout Hasher) {
649+
/// hasher.combine(a)
650+
/// }
651+
/// }
652+
/// ```
653+
@attached(extension, conformances: Hashable, names: named(hash))
654+
public macro ConformToHashable() = #externalMacro(module: "Macros", type: "ConformToHashable")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import SwiftSyntaxMacros
2+
import SwiftSyntaxMacrosTestSupport
3+
import XCTest
4+
import Macros
5+
6+
final class ConformToHashableTests: XCTestCase {
7+
let testMacros: [String: Macro.Type] = [
8+
"ConformToHashable": ConformToHashable.self,
9+
]
10+
11+
func testConformToEquatableMacro() {
12+
assertMacroExpansion(
13+
"""
14+
@ConformToHashable
15+
class AClass {
16+
let a: Int
17+
let b: Int
18+
init(a: Int, b: Int) {
19+
self.a = a
20+
self.b = b
21+
}
22+
}
23+
""",
24+
expandedSource:
25+
"""
26+
class AClass {
27+
let a: Int
28+
let b: Int
29+
init(a: Int, b: Int) {
30+
self.a = a
31+
self.b = b
32+
}
33+
}
34+
35+
extension AClass: Hashable {
36+
func hash(into hasher: inout Hasher) {
37+
hasher.combine(a)
38+
}
39+
}
40+
""",
41+
macros: testMacros
42+
)
43+
}
44+
45+
func testConformToEquatableIgnoreClosureTypeMacro() {
46+
assertMacroExpansion(
47+
"""
48+
@ConformToHashable
49+
class AClass {
50+
let a: Int
51+
let b: (Int) -> Void
52+
init(a: Int, b: (Int) -> Void) {
53+
self.a = a
54+
self.b = b
55+
}
56+
}
57+
""",
58+
expandedSource:
59+
"""
60+
class AClass {
61+
let a: Int
62+
let b: (Int) -> Void
63+
init(a: Int, b: (Int) -> Void) {
64+
self.a = a
65+
self.b = b
66+
}
67+
}
68+
69+
extension AClass: Hashable {
70+
func hash(into hasher: inout Hasher) {
71+
hasher.combine(a)
72+
}
73+
}
74+
""",
75+
macros: testMacros
76+
)
77+
}
78+
79+
func testConformToEquatableIncludeOptionalTypeMacro() {
80+
assertMacroExpansion(
81+
"""
82+
@ConformToHashable
83+
class AClass {
84+
let a: Int?
85+
init(a: Int?) {
86+
self.a = a
87+
}
88+
}
89+
""",
90+
expandedSource:
91+
"""
92+
class AClass {
93+
let a: Int?
94+
init(a: Int?) {
95+
self.a = a
96+
}
97+
}
98+
99+
extension AClass: Hashable {
100+
func hash(into hasher: inout Hasher) {
101+
hasher.combine(a)
102+
}
103+
}
104+
""",
105+
macros: testMacros
106+
)
107+
}
108+
}

0 commit comments

Comments
 (0)