Skip to content

Commit

Permalink
Support nested types, automatic for structs
Browse files Browse the repository at this point in the history
Fixes #12.
  • Loading branch information
JosephDuffy committed May 26, 2024
1 parent 03ee22a commit 1160398
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 21 deletions.
6 changes: 6 additions & 0 deletions Sources/HashableMacro/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import HashableMacroFoundation
/// `isEqual(to:)` function from Objective-C. Defaults to using the name of
/// the class the macro is attached to. This only applies to types that
/// conform to `NSObjectProtocol`.
/// - parameter fullyQualifiedName: The fully qualified name of the type. This is required when
/// adding the macro to a class that is nested in another type.
/// - parameter allowEmptyImplementation: When `nil` (default) and there are no
/// properties that contribute to the `Hashable` conformance the macro will
/// produce a warning. When `true` this warning is suppressed. When `false`
Expand All @@ -40,6 +42,7 @@ import HashableMacroFoundation
public macro Hashable(
finalHashInto: Bool = true,
isEqualToTypeFunctionName: IsEqualToTypeFunctionNameGeneration = .automatic,
fullyQualifiedName: String? = nil,
allowEmptyImplementation: Bool? = nil
) = #externalMacro(module: "HashableMacroMacros", type: "HashableMacro")
#else
Expand All @@ -56,13 +59,16 @@ public macro Hashable(
/// class, the `hash(into:)` function will be marked `final`. This helps avoid
/// a pitfall when subclassing an `Equatable` class: the `==` function cannot
/// be overridden in a subclass and `==` will always use the superclass.
/// - parameter fullyQualifiedName: The fully qualified name of the type. This is required when
/// adding the macro to a class that is nested in another type.
/// - parameter allowEmptyImplementation: When `nil` (default) and there are no
/// properties that contribute to the `Hashable` conformance the macro will
/// produce a warning. When `true` this warning is suppressed. When `false`
/// the warning will be elevated to an error.
@attached(extension, conformances: Hashable, Equatable, names: named(hash), named(==))
public macro Hashable(
finalHashInto: Bool = true,
fullyQualifiedName: String? = nil,
allowEmptyImplementation: Bool? = nil
) = #externalMacro(module: "HashableMacroMacros", type: "HashableMacro")
#endif
Expand Down
29 changes: 27 additions & 2 deletions Sources/HashableMacroMacros/Macros/HashableMacro+Hashable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,42 @@ extension HashableMacro {
)
}

var customFullyQualifiedName: String?

if let arguments = node.arguments?.as(LabeledExprListSyntax.self) {
for argument in arguments {
guard let label = argument.label else { continue }
switch label.trimmed.text {
case "fullyQualifiedName":
guard let stringExpression = argument.expression.as(StringLiteralExprSyntax.self) else { continue }
customFullyQualifiedName = "\(stringExpression.segments)"
default:
break
}
}
}

let hashableType: IdentifierTypeSyntax

if let customFullyQualifiedName {
hashableType = IdentifierTypeSyntax(name: .identifier(customFullyQualifiedName))
} else if declaration.is(StructDeclSyntax.self) {
hashableType = IdentifierTypeSyntax(name: .keyword(.Self))
} else {
hashableType = IdentifierTypeSyntax(name: .identifier(namedDeclaration.name.text))
}

let equalsFunctionSignature = FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(
parameters: [
FunctionParameterSyntax(
firstName: .identifier("lhs"),
type: TypeSyntax(stringLiteral: namedDeclaration.name.text),
type: hashableType,
trailingComma: .commaToken()
),
FunctionParameterSyntax(
firstName: .identifier("rhs"),
type: TypeSyntax(stringLiteral: namedDeclaration.name.text)
type: hashableType
),
]
),
Expand Down
17 changes: 17 additions & 0 deletions Tests/HashableMacroTests/HashableMacroAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ struct HashableStructWithExplictlyHashedComputedProperty {
}
}

enum OuterType {
@Hashable
struct InnerStruct {
let hashedProperty: String
}

@Hashable(fullyQualifiedName: "OuterType.InnerClass")
class InnerClass {
@Hashed
let hashedProperty: String

init(hashedProperty: String) {
self.hashedProperty = hashedProperty
}
}
}

@Hashable
public class HashableClassWithPrivateProperty: Hashable {
@Hashed
Expand Down
105 changes: 86 additions & 19 deletions Tests/HashableMacroTests/HashableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,73 @@ final class HashableMacroTests: XCTestCase {
#endif
}

func testEmbeddedType() throws {
#if canImport(HashableMacroMacros)
assertMacro(testMacros) {
"""
enum Outer {
@Hashable(_disableNSObjectSubclassSupport: true)
struct InnerStruct {
let hashedProperty: String
}
@Hashable(fullyQualifiedName: "Outer.InnerClass", _disableNSObjectSubclassSupport: true)
class InnerClass {
@Hashed
let hashedProperty: String
init(hashedProperty: String) {
self.hashedProperty = hashedProperty
}
}
}
"""
} expansion: {
// This should be e.g. `extension Outer.InnerStruct` but the tester does not output this.
"""
enum Outer {
struct InnerStruct {
let hashedProperty: String
}
class InnerClass {
let hashedProperty: String
init(hashedProperty: String) {
self.hashedProperty = hashedProperty
}
}
}
extension InnerStruct {
func hash(into hasher: inout Hasher) {
hasher.combine(self.hashedProperty)
}
}
extension InnerStruct {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
extension InnerClass {
final func hash(into hasher: inout Hasher) {
hasher.combine(self.hashedProperty)
}
}
extension InnerClass {
static func ==(lhs: Outer.InnerClass, rhs: Outer.InnerClass) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
"""
}
#else
throw XCTSkip("Macros are only supported when running tests for the host platform")
#endif
}

func testPublicStruct() throws {
#if canImport(HashableMacroMacros)
assertMacro(testMacros) {
Expand All @@ -211,7 +278,7 @@ final class HashableMacroTests: XCTestCase {
}
extension PublicStruct {
public static func ==(lhs: PublicStruct, rhs: PublicStruct) -> Bool {
public static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashableProperty == rhs.hashableProperty
}
}
Expand Down Expand Up @@ -245,7 +312,7 @@ final class HashableMacroTests: XCTestCase {
}
extension PackageStruct {
package static func ==(lhs: PackageStruct, rhs: PackageStruct) -> Bool {
package static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashableProperty == rhs.hashableProperty
}
}
Expand Down Expand Up @@ -279,7 +346,7 @@ final class HashableMacroTests: XCTestCase {
}
extension ExplicitInternalStruct {
internal static func ==(lhs: ExplicitInternalStruct, rhs: ExplicitInternalStruct) -> Bool {
internal static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashableProperty == rhs.hashableProperty
}
}
Expand Down Expand Up @@ -313,7 +380,7 @@ final class HashableMacroTests: XCTestCase {
}
extension FileprivateStruct {
fileprivate static func ==(lhs: FileprivateStruct, rhs: FileprivateStruct) -> Bool {
fileprivate static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashableProperty == rhs.hashableProperty
}
}
Expand Down Expand Up @@ -347,7 +414,7 @@ final class HashableMacroTests: XCTestCase {
}
extension PrivateStruct {
static func ==(lhs: PrivateStruct, rhs: PrivateStruct) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashableProperty == rhs.hashableProperty
}
}
Expand Down Expand Up @@ -399,7 +466,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TypeNotExplicitlyConformingToHashable {
static func ==(lhs: TypeNotExplicitlyConformingToHashable, rhs: TypeNotExplicitlyConformingToHashable) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashablePropery1 == rhs.hashablePropery1
&& lhs.hashablePropery2 == rhs.hashablePropery2
&& lhs.hashablePropery3 == rhs.hashablePropery3
Expand Down Expand Up @@ -434,7 +501,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TypeWithoutHashableKeys {
static func ==(lhs: TypeWithoutHashableKeys, rhs: TypeWithoutHashableKeys) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.notHashedProperty == rhs.notHashedProperty
}
}
Expand Down Expand Up @@ -468,7 +535,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TypeWithExplicitHashableConformation {
static func ==(lhs: TypeWithExplicitHashableConformation, rhs: TypeWithExplicitHashableConformation) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -502,7 +569,7 @@ final class HashableMacroTests: XCTestCase {
}
extension PublicType {
public static func ==(lhs: PublicType, rhs: PublicType) -> Bool {
public static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -536,7 +603,7 @@ final class HashableMacroTests: XCTestCase {
}
extension ExplicitlyInternalType {
internal static func ==(lhs: ExplicitlyInternalType, rhs: ExplicitlyInternalType) -> Bool {
internal static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -570,7 +637,7 @@ final class HashableMacroTests: XCTestCase {
}
extension FilePrivateType {
fileprivate static func ==(lhs: FilePrivateType, rhs: FilePrivateType) -> Bool {
fileprivate static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -604,7 +671,7 @@ final class HashableMacroTests: XCTestCase {
}
extension PrivateType {
static func ==(lhs: PrivateType, rhs: PrivateType) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -639,7 +706,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TestStruct {
static func ==(lhs: TestStruct, rhs: TestStruct) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
&& lhs.secondHashedProperty == rhs.secondHashedProperty
}
Expand Down Expand Up @@ -692,7 +759,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TestStruct {
static func ==(lhs: TestStruct, rhs: TestStruct) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return true
}
}
Expand Down Expand Up @@ -892,7 +959,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TypeWithComputedPropertt {
static func ==(lhs: TypeWithComputedPropertt, rhs: TypeWithComputedPropertt) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -938,7 +1005,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TypeWithComputedPropertt {
static func ==(lhs: TypeWithComputedPropertt, rhs: TypeWithComputedPropertt) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -984,7 +1051,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TypeWithComputedPropertt {
static func ==(lhs: TypeWithComputedPropertt, rhs: TypeWithComputedPropertt) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
&& lhs.otherHashedProperty == rhs.otherHashedProperty
}
Expand Down Expand Up @@ -1046,7 +1113,7 @@ final class HashableMacroTests: XCTestCase {
}
extension TypeWithMixedHashedNotHashed {
static func ==(lhs: TypeWithMixedHashedNotHashed, rhs: TypeWithMixedHashedNotHashed) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashedProperty == rhs.hashedProperty
}
}
Expand Down Expand Up @@ -1162,7 +1229,7 @@ final class HashableMacroTests: XCTestCase {
}
extension Test {
static func ==(lhs: Test, rhs: Test) -> Bool {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.hashablePropery == rhs.hashablePropery
}
}
Expand Down

0 comments on commit 1160398

Please sign in to comment.