diff --git a/Sources/CasePathsMacrosSupport/CasePathableMacro.swift b/Sources/CasePathsMacrosSupport/CasePathableMacro.swift index 00f90b3d..eab76352 100644 --- a/Sources/CasePathsMacrosSupport/CasePathableMacro.swift +++ b/Sources/CasePathsMacrosSupport/CasePathableMacro.swift @@ -6,6 +6,11 @@ import SwiftSyntaxMacros public struct CasePathableMacro { static let moduleName = "CasePaths" static let casePathTypeName = "AnyCasePath" + static let casePathableExpandingMacroNames = [ + "CaseBindable", + "Table", + "Selection" + ] private static func shouldGenerate( for node: AttributeSyntax, @@ -15,11 +20,26 @@ public struct CasePathableMacro { type.as(IdentifierTypeSyntax.self)?.name.text == "CasePathable" || type.as(MemberTypeSyntax.self)?.name.text == "CasePathable" } - if isCasePathable(node.attributeName) { return true } - return !declaration.attributes.contains { element in - guard case let .attribute(attribute) = element else { return false } - return isCasePathable(attribute.attributeName) + guard !isCasePathable(node.attributeName) + else { return true } + + let hasCasePathableApplied = declaration.attributes.contains { + $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "CasePathable" + } + let firstCasePathableExpandableMacroIndex = casePathableExpandingMacroNames.compactMap { macroName in + declaration.attributes.firstIndex { + $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == + macroName + } } + .sorted() + .first + let nodeIndex = declaration.attributes.firstIndex { + $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == + node.attributeName.as(IdentifierTypeSyntax.self)?.name.text ?? "" + } + + return !hasCasePathableApplied && firstCasePathableExpandableMacroIndex == nodeIndex } } diff --git a/Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift b/Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift index 55ba20ad..f4882be8 100644 --- a/Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift +++ b/Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift @@ -5,62 +5,11 @@ import SwiftSyntaxMacros import Testing - private enum CaseBindableMacro {} - - extension CaseBindableMacro: ExtensionMacro { - static func expansion( - of node: AttributeSyntax, - attachedTo declaration: some DeclGroupSyntax, - providingExtensionsOf type: some TypeSyntaxProtocol, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [ExtensionDeclSyntax] { - try CasePathableMacro.expansion( - of: node, - attachedTo: declaration, - providingExtensionsOf: type, - conformingTo: protocols, - in: context - ) - } - } - - extension CaseBindableMacro: MemberMacro { - static func expansion( - of node: AttributeSyntax, - providingMembersOf declaration: some DeclGroupSyntax, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - var decls = try CasePathableMacro.expansion( - of: node, - providingMembersOf: declaration, - in: context - ) - guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { return decls } - let elements = enumDecl.memberBlock.members - .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } - let cases = elements.map { element -> String in - let hasPayload = element.parameterClause.map { !$0.parameters.isEmpty } ?? false - guard hasPayload else { return "case \(element.name.text)" } - let type = CasePathableMacro.valueType(for: element) - return "case \(element.name.text)(SwiftUI.Binding<\(type)>)" - } - decls.append( - """ - public enum BindingEnumeration { - \(raw: cases.joined(separator: "\n")) - } - """ - ) - return decls - } - } - @Suite( .macros([ CaseBindableMacro.self, CasePathableMacro.self, + SelectionMacro.self, ]) ) struct CasePathsMacrosSupportTests { @@ -225,5 +174,253 @@ """# } } + + + @Test + func `multiple macros that expand @CasePathable without @CasePathable`() { + assertMacro { + """ + @CaseBindable + @Selection + enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + } + """ + } expansion: { + #""" + enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + + public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { + public subscript(root: Foo) -> CasePaths.PartialCaseKeyPath { + if root.is(\.bar) { + return \.bar + } + if root.is(\.baz) { + return \.baz + } + if root.is(\.fizz) { + return \.fizz + } + return \.never + } + public var bar: CasePaths.AnyCasePath { + ._$embed({ + Foo.bar + }) { + guard case .bar = $0 else { + return nil + } + return () + } + } + public var baz: CasePaths.AnyCasePath { + ._$embed(Foo.baz) { + guard case let .baz(v0) = $0 else { + return nil + } + return v0 + } + } + public var fizz: CasePaths.AnyCasePath { + ._$embed(Foo.fizz) { + guard case let .fizz(v0) = $0 else { + return nil + } + return v0 + } + } + public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath]> { + var allCasePaths: [CasePaths.PartialCaseKeyPath] = [] + allCasePaths.append(\.bar) + allCasePaths.append(\.baz) + allCasePaths.append(\.fizz) + return allCasePaths.makeIterator() + } + } + + public static var allCasePaths: AllCasePaths { + AllCasePaths() + } + + public enum BindingEnumeration { + case bar + case baz(SwiftUI.Binding) + case fizz(SwiftUI.Binding) + } + } + + extension Foo: CasePaths.CasePathable, CasePaths.CasePathIterable { + } + """# + } + } + + @Test + func `multiple macros that expand @CasePathable with @CasePathable`() { + assertMacro { + """ + @CasePathable + @CaseBindable + @Selection + enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + } + """ + } expansion: { + #""" + enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + + public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { + public subscript(root: Foo) -> CasePaths.PartialCaseKeyPath { + if root.is(\.bar) { + return \.bar + } + if root.is(\.baz) { + return \.baz + } + if root.is(\.fizz) { + return \.fizz + } + return \.never + } + public var bar: CasePaths.AnyCasePath { + ._$embed({ + Foo.bar + }) { + guard case .bar = $0 else { + return nil + } + return () + } + } + public var baz: CasePaths.AnyCasePath { + ._$embed(Foo.baz) { + guard case let .baz(v0) = $0 else { + return nil + } + return v0 + } + } + public var fizz: CasePaths.AnyCasePath { + ._$embed(Foo.fizz) { + guard case let .fizz(v0) = $0 else { + return nil + } + return v0 + } + } + public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath]> { + var allCasePaths: [CasePaths.PartialCaseKeyPath] = [] + allCasePaths.append(\.bar) + allCasePaths.append(\.baz) + allCasePaths.append(\.fizz) + return allCasePaths.makeIterator() + } + } + + public static var allCasePaths: AllCasePaths { + AllCasePaths() + } + + public enum BindingEnumeration { + case bar + case baz(SwiftUI.Binding) + case fizz(SwiftUI.Binding) + } + } + + extension Foo: CasePaths.CasePathable, CasePaths.CasePathIterable { + } + """# + } + } + } + + private enum SelectionMacro: ExtensionMacro, MemberMacro { + static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + try CasePathableMacro.expansion( + of: node, + attachedTo: declaration, + providingExtensionsOf: type, + conformingTo: protocols, + in: context + ) + } + static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + try CasePathableMacro.expansion( + of: node, + providingMembersOf: declaration, + in: context + ) + } + } + + private enum CaseBindableMacro: MemberMacro, ExtensionMacro { + static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + try CasePathableMacro.expansion( + of: node, + attachedTo: declaration, + providingExtensionsOf: type, + conformingTo: protocols, + in: context + ) + } + + static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + var decls = try CasePathableMacro.expansion( + of: node, + providingMembersOf: declaration, + in: context + ) + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { return decls } + let elements = enumDecl.memberBlock.members + .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } + let cases = elements.map { element -> String in + let hasPayload = element.parameterClause.map { !$0.parameters.isEmpty } ?? false + guard hasPayload else { return "case \(element.name.text)" } + let type = CasePathableMacro.valueType(for: element) + return "case \(element.name.text)(SwiftUI.Binding<\(type)>)" + } + decls.append( + """ + public enum BindingEnumeration { + \(raw: cases.joined(separator: "\n")) + } + """ + ) + return decls + } } #endif