Skip to content

Commit

Permalink
Add @ConformToHashable
Browse files Browse the repository at this point in the history
Minor format adjust
  • Loading branch information
ShenghaiWang committed Dec 18, 2023
1 parent d5213ca commit bc210ec
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 6 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ A practical collection of Swift Macros that help code correctly and swiftly.
| |<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>|
| #buildURLRequest |Build a URLRequest from components.<br>This solution addes in a resultBulder `URLRequestBuilder`, which can be used directly if prefer builder pattern. |
| |<pre>let urlrequest = #buildURLRequest(url!, RequestTimeOutInterval(100))<br>let urlRequest2 = buildURLRequest {<br> url!<br> RequestTimeOutInterval(100)<br>}</pre>|
| @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|
| @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|
| |<pre>@AddInit<br>@ConformToEquatable<br>class AClass {<br> let a: Int?<br> let b: () -> Void<br>}</pre>|
| @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|
| |<pre>@AddInit<br>@ConformToHashable<br>class AClass {<br> let a: Int?<br> let b: () -> Void<br>}</pre>|
| #encode |Encode an Encodable to data using JSONEncoder |
| |<pre>#encode(value)</pre>|
| #decode |Decode a Decodable to a typed value using JSONDecoder |
Expand Down
2 changes: 1 addition & 1 deletion Sources/Client/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ struct TestAccess {
var keychainValue: TestStruct?
}


@AddInit
@ConformToEquatable
@ConformToHashable
class AClass {
let a: Int?
let b: () -> Void
Expand Down
47 changes: 47 additions & 0 deletions Sources/Macros/ConformToHashable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import Foundation

public struct ConformToHashable: ExtensionMacro {
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo protocols: [SwiftSyntax.TypeSyntax],
in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
guard [SwiftSyntax.SyntaxKind.classDecl].contains(declaration.kind) else {
throw MacroDiagnostics.errorMacroUsage(message: "Can only be applied to a class type")
}
let equatableProtocol = InheritanceClauseSyntax(inheritedTypes: InheritedTypeListSyntax(
arrayLiteral: InheritedTypeSyntax(type: TypeSyntax(stringLiteral: "Hashable")))
)

let mambers = declaration.memberBlock.members.compactMap { member in
if let patternBinding = member.decl.as(VariableDeclSyntax.self)?.bindings
.as(PatternBindingListSyntax.self)?.first?.as(PatternBindingSyntax.self),
let identifier = patternBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
let type = patternBinding.typeAnnotation?.as(TypeAnnotationSyntax.self)?.type {
if type.is(IdentifierTypeSyntax.self) {
return "hasher.combine(\(identifier))"
}
if let wrappedType = type.as(OptionalTypeSyntax.self)?.wrappedType,
wrappedType.is(IdentifierTypeSyntax.self) {
return "hasher.combine(\(identifier))"
}
}
return nil
}.joined(separator: "\n ")

let equtableFunction = """
func hash(into hasher: inout Hasher) {
\(mambers)
}
"""

let member = MemberBlockSyntax(members: MemberBlockItemListSyntax(stringLiteral: equtableFunction))
let extensionDecl = ExtensionDeclSyntax(extendedType: type,
inheritanceClause: equatableProtocol,
memberBlock: member)
return [extensionDecl]
}
}
3 changes: 2 additions & 1 deletion Sources/Macros/MacroPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct MacroPlugin: CompilerPlugin {
Mock.self,
PostNotification.self,
Singleton.self,
ConformToEquatable.self
ConformToEquatable.self,
ConformToHashable.self
]
}
3 changes: 2 additions & 1 deletion Sources/SwiftMacros/SwiftMacros.docc/SwiftMacros.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ This collection of Swift Macros aims to remove boilerplate code by automatically
- ``buildDate(_:)``
- ``buildURL(_:)``
- ``buildURLRequest(_:)``
- ``ConformToEqutable``
- ``ConformToHashable``
- ``encode(_:outputFormatting:dateEncodingStrategy:dataEncodingStrategy:nonConformingFloatEncodingStrategy:keyEncodingStrategy:userInfo:)``
- ``decode(_:from:dateDecodingStrategy:dataDecodingStrategy:nonConformingFloatDecodingStrategy:keyDecodingStrategy:userInfo:allowsJSON5:assumesTopLevelDictionary:)``
- ``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:)``
Expand All @@ -32,5 +34,4 @@ This collection of Swift Macros aims to remove boilerplate code by automatically
- ``Mock(type:randomMockValue:)``
- ``postNotification(_:object:userInfo:from:)``
- ``Singleton``
- ``ConformToEqutable``

39 changes: 37 additions & 2 deletions Sources/SwiftMacros/SwiftMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -607,13 +607,48 @@ public macro Singleton() = #externalMacro(module: "Macros", type: "Singleton")
/// ```
/// will expand to
/// ```swift
/// class A: Equtable {
/// class A {
/// let a: Int
/// init(a: Int) {
/// self.a = a
/// }
///
/// }
///
/// extension A: Equatable {
/// static func == (lhs: A, rhs: A) -> Bool {
/// lhs.a == rhs.a
/// }
/// }
/// ```
@attached(extension, conformances: Equatable, names: named(==))
public macro ConformToEquatable() = #externalMacro(module: "Macros", type: "ConformToEquatable")

/// Add Hashable conformance to class
///
/// For example:
/// ```swift
/// @ConformToHashable
/// class A {
/// let a: Int
/// init(a: Int) {
/// self.a = a
/// }
/// }
/// ```
/// will expand to
/// ```swift
/// class A {
/// let a: Int
/// init(a: Int) {
/// self.a = a
/// }
/// }
///
/// extension A: Hashable {
/// func hash(into hasher: inout Hasher) {
/// hasher.combine(a)
/// }
/// }
/// ```
@attached(extension, conformances: Hashable, names: named(hash))
public macro ConformToHashable() = #externalMacro(module: "Macros", type: "ConformToHashable")
108 changes: 108 additions & 0 deletions Tests/MacroTests/ConformToHashableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
import Macros

final class ConformToHashableTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"ConformToHashable": ConformToHashable.self,
]

func testConformToEquatableMacro() {
assertMacroExpansion(
"""
@ConformToHashable
class AClass {
let a: Int
let b: Int
init(a: Int, b: Int) {
self.a = a
self.b = b
}
}
""",
expandedSource:
"""
class AClass {
let a: Int
let b: Int
init(a: Int, b: Int) {
self.a = a
self.b = b
}
}
extension AClass: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(a)
}
}
""",
macros: testMacros
)
}

func testConformToEquatableIgnoreClosureTypeMacro() {
assertMacroExpansion(
"""
@ConformToHashable
class AClass {
let a: Int
let b: (Int) -> Void
init(a: Int, b: (Int) -> Void) {
self.a = a
self.b = b
}
}
""",
expandedSource:
"""
class AClass {
let a: Int
let b: (Int) -> Void
init(a: Int, b: (Int) -> Void) {
self.a = a
self.b = b
}
}
extension AClass: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(a)
}
}
""",
macros: testMacros
)
}

func testConformToEquatableIncludeOptionalTypeMacro() {
assertMacroExpansion(
"""
@ConformToHashable
class AClass {
let a: Int?
init(a: Int?) {
self.a = a
}
}
""",
expandedSource:
"""
class AClass {
let a: Int?
init(a: Int?) {
self.a = a
}
}
extension AClass: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(a)
}
}
""",
macros: testMacros
)
}
}

0 comments on commit bc210ec

Please sign in to comment.