From e1505212537159f733d7673bb80601e95bdf6a6d Mon Sep 17 00:00:00 2001 From: Rob Amos Date: Mon, 1 Dec 2025 20:10:30 +1100 Subject: [PATCH 1/2] Inline with SE-0185 we should generate Equatable conformance even we're empty --- Sources/VexilMacros/FlagContainerMacro.swift | 16 ++++++++-------- .../EquatableFlagContainerMacroTests.swift | 8 +++++++- .../FlagContainerMacroTests.swift | 18 ++++++++++++++++++ Tests/VexilTests/EquatableTests.swift | 9 +++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Sources/VexilMacros/FlagContainerMacro.swift b/Sources/VexilMacros/FlagContainerMacro.swift index 512b4ed9..7bb8a094 100644 --- a/Sources/VexilMacros/FlagContainerMacro.swift +++ b/Sources/VexilMacros/FlagContainerMacro.swift @@ -78,11 +78,6 @@ extension FlagContainerMacro: ExtensionMacro { shouldGenerateConformance.equatable = false } - // We also can't generate Equatable conformance if there is no variables to generate them - if shouldGenerateConformance.equatable, declaration.memberBlock.variables.isEmpty { - shouldGenerateConformance.equatable = false - } - // Check that conformance doesn't already exist, or that we are inside a unit test. // The latter is a workaround for https://github.com/apple/swift-syntax/issues/2031 guard shouldGenerateConformance.flagContainer else { @@ -159,8 +154,8 @@ extension FlagContainerMacro: ExtensionMacro { inheritanceClause: .init(inheritedTypes: [ .init(type: TypeSyntax(stringLiteral: "Equatable")) ]) ) { var variables = declaration.memberBlock.storedVariables - if variables.isEmpty == false { - try FunctionDeclSyntax("func ==(lhs: \(type), rhs: \(type)) -> Bool") { + try FunctionDeclSyntax("func ==(lhs: \(type), rhs: \(type)) -> Bool") { + if variables.isEmpty == false { if let lastBinding = variables.removeLast().bindings.first?.pattern { for variable in variables { if let binding = variable.bindings.first?.pattern { @@ -172,9 +167,14 @@ extension FlagContainerMacro: ExtensionMacro { } ExprSyntax("lhs.\(lastBinding.trimmed) == rhs.\(lastBinding.trimmed)") } + + // If there are no stored properties just return `true`. This matches the synthesised Equatable + // behaviour described in https://github.com/swiftlang/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details + } else { + "true" } - .with(\.modifiers, Array(scopes) + [ DeclModifierSyntax(name: .keyword(.static)) ]) } + .with(\.modifiers, Array(scopes) + [ DeclModifierSyntax(name: .keyword(.static)) ]) }, ] } diff --git a/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift b/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift index 0e94a8dd..0d372f1c 100644 --- a/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift +++ b/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift @@ -20,7 +20,7 @@ import XCTest final class EquatableFlagContainerMacroTests: XCTestCase { - func testDoesntGenerateWhenEmpty() throws { + func testGeneratesWhenEmpty() throws { assertMacroExpansion( """ @FlagContainer @@ -50,6 +50,12 @@ final class EquatableFlagContainerMacroTests: XCTestCase { [:] } } + + extension TestFlags: Equatable { + static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { + true + } + } """, macros: [ "FlagContainer": FlagContainerMacro.self, diff --git a/Tests/VexilMacroTests/FlagContainerMacroTests.swift b/Tests/VexilMacroTests/FlagContainerMacroTests.swift index fee0a488..7a4d0f94 100644 --- a/Tests/VexilMacroTests/FlagContainerMacroTests.swift +++ b/Tests/VexilMacroTests/FlagContainerMacroTests.swift @@ -50,6 +50,12 @@ final class FlagContainerMacroTests: XCTestCase { [:] } } + + extension TestFlags: Equatable { + static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { + true + } + } """, macros: [ "FlagContainer": FlagContainerMacro.self, @@ -87,6 +93,12 @@ final class FlagContainerMacroTests: XCTestCase { [:] } } + + extension TestFlags: Equatable { + public static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { + true + } + } """, macros: [ "FlagContainer": FlagContainerMacro.self, @@ -124,6 +136,12 @@ final class FlagContainerMacroTests: XCTestCase { [:] } } + + extension TestFlags: Equatable { + static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { + true + } + } """, macros: [ "FlagContainer": FlagContainerMacro.self, diff --git a/Tests/VexilTests/EquatableTests.swift b/Tests/VexilTests/EquatableTests.swift index a0a509cf..71d3b9d9 100644 --- a/Tests/VexilTests/EquatableTests.swift +++ b/Tests/VexilTests/EquatableTests.swift @@ -176,4 +176,13 @@ private struct DoubleSubgroupFlags { @Flag("Third level test flag") var thirdLevelFlag = false + @FlagGroup("Empty flags") + var empty: EmptyFlags + +} + +// Support for empty Flag Containers +@FlagContainer +private struct EmptyFlags { + // Intentionally left blank } From a2aaadb5be8bed7a5481497c6282e4dd3010f3a1 Mon Sep 17 00:00:00 2001 From: Rob Amos Date: Mon, 1 Dec 2025 20:26:15 +1100 Subject: [PATCH 2/2] Fixed formatting --- Sources/VexilMacros/FlagContainerMacro.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/VexilMacros/FlagContainerMacro.swift b/Sources/VexilMacros/FlagContainerMacro.swift index 7bb8a094..17c7342f 100644 --- a/Sources/VexilMacros/FlagContainerMacro.swift +++ b/Sources/VexilMacros/FlagContainerMacro.swift @@ -168,8 +168,8 @@ extension FlagContainerMacro: ExtensionMacro { ExprSyntax("lhs.\(lastBinding.trimmed) == rhs.\(lastBinding.trimmed)") } - // If there are no stored properties just return `true`. This matches the synthesised Equatable - // behaviour described in https://github.com/swiftlang/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details + // If there are no stored properties just return `true`. This matches the synthesised Equatable + // behaviour described in https://github.com/swiftlang/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details } else { "true" }