From 656fa9ce2e7026607a32fadfb8843c4f6605b206 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 26 Feb 2024 13:02:33 -0800 Subject: [PATCH] Support `#if` branching in enum reducers/state (#2800) * Support `#if` branching in enum reducers/state This adds support for `#if` branching across enum cases in observable state and reducer enums. * wip * add tests --------- Co-authored-by: Brandon Williams --- .../ObservableStateMacro.swift | 142 +++++-- .../ReducerMacro.swift | 388 +++++++++++++----- .../ObservableStateMacroTests.swift | 87 ++++ .../ReducerMacroTests.swift | 274 ++++++++++--- .../EnumReducerMacroTests.swift | 31 ++ .../MacroTests.swift | 4 +- .../ObservableStateEnumMacroTests.swift | 22 + 7 files changed, 751 insertions(+), 197 deletions(-) create mode 100644 Tests/ComposableArchitectureTests/EnumReducerMacroTests.swift create mode 100644 Tests/ComposableArchitectureTests/ObservableStateEnumMacroTests.swift diff --git a/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift b/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift index 16042160d710..5c7f43b7f730 100644 --- a/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift +++ b/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift @@ -248,9 +248,6 @@ extension ObservableStateMacro: MemberMacro { var declarations = [DeclSyntax]() - let access = declaration.modifiers.first { - $0.name.tokenKind == .keyword(.public) || $0.name.tokenKind == .keyword(.package) - } declaration.addIfNeeded( ObservableStateMacro.registrarVariable(observableType), to: &declarations) declaration.addIfNeeded(ObservableStateMacro.idVariable(), to: &declarations) @@ -260,6 +257,106 @@ extension ObservableStateMacro: MemberMacro { } } +extension Array where Element == ObservableStateCase { + init(members: MemberBlockItemListSyntax) { + var tag = 0 + self.init(members: members, tag: &tag) + } + + init(members: MemberBlockItemListSyntax, tag: inout Int) { + self = members.flatMap { member -> [ObservableStateCase] in + if let enumCaseDecl = member.decl.as(EnumCaseDeclSyntax.self) { + return enumCaseDecl.elements.map { + defer { tag += 1 } + return ObservableStateCase.element($0, tag: tag) + } + } + if let ifConfigDecl = member.decl.as(IfConfigDeclSyntax.self) { + let configs = ifConfigDecl.clauses.flatMap { decl -> [ObservableStateCase.IfConfig] in + guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) + else { return [] } + return [ + ObservableStateCase.IfConfig( + poundKeyword: decl.poundKeyword, + condition: decl.condition, + cases: Array(members: elements, tag: &tag) + ) + ] + } + return [.ifConfig(configs)] + } + return [] + } + } +} + +enum ObservableStateCase { + case element(EnumCaseElementSyntax, tag: Int) + indirect case ifConfig([IfConfig]) + + struct IfConfig { + let poundKeyword: TokenSyntax + let condition: ExprSyntax? + let cases: [ObservableStateCase] + } + + var getCase: String { + switch self { + case let .element(element, tag): + if let parameters = element.parameterClause?.parameters, parameters.count == 1 { + return """ + case let .\(element.name.text)(state): + return ._$id(for: state)._$tag(\(tag)) + """ + } else { + return """ + case .\(element.name.text): + return ._$inert._$tag(\(tag)) + """ + } + case let .ifConfig(configs): + return configs + .map { + """ + \($0.poundKeyword.text) \($0.condition?.trimmedDescription ?? "") + \($0.cases.map(\.getCase).joined(separator: "\n")) + """ + } + .joined(separator: "\n") + "#endif\n" + } + } + + var willModifyCase: String { + switch self { + case let .element(element, _): + if let parameters = element.parameterClause?.parameters, + parameters.count == 1, + let parameter = parameters.first + { + return """ + case var .\(element.name.text)(state): + \(ObservableStateMacro.moduleName)._$willModify(&state) + self = .\(element.name.text)(\(parameter.firstName.map { "\($0): " } ?? "")state) + """ + } else { + return """ + case .\(element.name.text): + break + """ + } + case let .ifConfig(configs): + return configs + .map { + """ + \($0.poundKeyword.text) \($0.condition?.trimmedDescription ?? "") + \($0.cases.map(\.willModifyCase).joined(separator: "\n")) + """ + } + .joined(separator: "\n") + "#endif\n" + } + } +} + extension ObservableStateMacro { public static func enumExpansion< Declaration: DeclGroupSyntax, @@ -269,43 +366,12 @@ extension ObservableStateMacro { providingMembersOf declaration: Declaration, in context: Context ) throws -> [DeclSyntax] { - let enumCaseDecls = declaration.memberBlock.members - .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } + let cases = [ObservableStateCase](members: declaration.memberBlock.members) var getCases: [String] = [] var willModifyCases: [String] = [] - for (tag, enumCaseDecl) in enumCaseDecls.enumerated() { - // TODO: Support multiple parameters of observable state? - if let parameters = enumCaseDecl.parameterClause?.parameters, - parameters.count == 1, - let parameter = parameters.first - { - getCases.append( - """ - case let .\(enumCaseDecl.name.text)(state): - return ._$id(for: state)._$tag(\(tag)) - """ - ) - willModifyCases.append( - """ - case var .\(enumCaseDecl.name.text)(state): - \(moduleName)._$willModify(&state) - self = .\(enumCaseDecl.name.text)(\(parameter.firstName.map { "\($0): " } ?? "")state) - """ - ) - } else { - getCases.append( - """ - case .\(enumCaseDecl.name.text): - return ._$inert._$tag(\(tag)) - """ - ) - willModifyCases.append( - """ - case .\(enumCaseDecl.name.text): - break - """ - ) - } + for enumCase in cases { + getCases.append(enumCase.getCase) + willModifyCases.append(enumCase.willModifyCase) } return [ diff --git a/Sources/ComposableArchitectureMacros/ReducerMacro.swift b/Sources/ComposableArchitectureMacros/ReducerMacro.swift index 5f1a99469379..9d2abc28f2b9 100644 --- a/Sources/ComposableArchitectureMacros/ReducerMacro.swift +++ b/Sources/ComposableArchitectureMacros/ReducerMacro.swift @@ -236,87 +236,25 @@ extension ReducerMacro: MemberMacro { } || hasReduceMethod var decls: [DeclSyntax] = [] if let enumDecl = declaration.as(EnumDeclSyntax.self) { - let enumCaseElements = enumDecl.memberBlock - .members - .flatMap { member -> [ReducerCase] in - guard let decl = member.decl.as(EnumCaseDeclSyntax.self) else { return [] } - return decl.elements.map { - ReducerCase(element: $0, attribute: decl.attribute) - } - } - + let enumCaseElements = [ReducerCase](members: enumDecl.memberBlock.members) var stateCaseDecls: [String] = [] var actionCaseDecls: [String] = [] + var reducerType: ReducerCase.Body = .scoped([]) var reducerScopes: [String] = [] var storeCases: [String] = [] var storeScopes: [String] = [] - var reducerTypeScopes: [String] = [] for enumCaseElement in enumCaseElements { - let element = enumCaseElement.element - let name = element.name.text - - if enumCaseElement.attribute != .ignored, - let parameterClause = element.parameterClause, - parameterClause.parameters.count == 1, - let parameter = parameterClause.parameters.first, - parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self) - { - let type = parameter.type - let stateCase = - enumCaseElement.attribute == .ephemeral - ? element - : element.suffixed("State") - stateCaseDecls.append("case \(stateCase.trimmedDescription)") - actionCaseDecls.append("case \(element.suffixed("Action").trimmedDescription)") - if enumCaseElement.attribute == nil { - reducerScopes.append( - """ - ComposableArchitecture.Scope(\ - state: \\Self.State.Cases.\(name), action: \\Self.Action.Cases.\(name)\ - ) { - \(type.trimmed)() - } - """ - ) - storeCases.append("case \(name)(ComposableArchitecture.StoreOf<\(type.trimmed)>)") - storeScopes.append( - """ - case .\(name): - return .\(name)(store.scope(state: \\.\(name), action: \\.\(name))!) - """ - ) - reducerTypeScopes.append( - """ - ComposableArchitecture.Scope - """ - ) - } - } else { - stateCaseDecls.append("case \(element.trimmedDescription)") + stateCaseDecls.append(enumCaseElement.stateCaseDecl) + if let actionCaseDecl = enumCaseElement.actionCaseDecl { + actionCaseDecls.append(actionCaseDecl) } - if enumCaseElement.attribute != nil { - storeCases.append("case \(element.trimmedDescription)") - if let parameters = element.parameterClause?.parameters { - let bindingNames = (0..._Sequence<" - ) - } - staticVarBody.append(reducerTypeScopes[0]) - staticVarBody.append(", ") - for type in reducerTypeScopes.dropFirst() { - staticVarBody.append(type) - staticVarBody.append(">, ") + switch reducerType { + case .erased: + staticVarBody = "Reduce" + case let .scoped(reducerTypeScopes): + if reducerTypeScopes.isEmpty { + staticVarBody = "ComposableArchitecture.EmptyReducer" + } else if reducerTypeScopes.count == 1 { + staticVarBody = reducerTypeScopes[0] + } else { + for _ in 1...(reducerTypeScopes.count - 1) { + staticVarBody.append( + "ComposableArchitecture.ReducerBuilder._Sequence<" + ) + } + staticVarBody.append(reducerTypeScopes[0]) + staticVarBody.append(", ") + for type in reducerTypeScopes.dropFirst() { + staticVarBody.append(type) + staticVarBody.append(">, ") + } + staticVarBody.removeLast(2) } - staticVarBody.removeLast(2) } - decls.append( - """ - @ComposableArchitecture.ReducerBuilder - \(access)static var body: \(raw: staticVarBody) { - """ - ) + var body = "" if reducerScopes.isEmpty { - decls.append( + body.append( """ ComposableArchitecture.EmptyReducer() - """) + """ + ) } else { - decls.append( + body.append( """ - \(raw: reducerScopes.joined(separator: "\n")) - - """) + \(reducerScopes.joined(separator: "\n")) + """ + ) } - decls.append("}") + if case .erased = reducerType { + body = """ + ComposableArchitecture.Reduce( + ComposableArchitecture.CombineReducers { + \(body) + } + ) + """ + } + decls.append( + """ + @ComposableArchitecture.ReducerBuilder + \(access)static var body: \(raw: staticVarBody) { + \(raw: body) + } + """ + ) } if !typeNames.contains("CaseScope") { decls.append( @@ -473,14 +428,239 @@ extension ReducerMacro: MemberMacro { } } -private struct ReducerCase { - let element: EnumCaseElementSyntax - let attribute: Attribute? +private enum ReducerCase { + case element(EnumCaseElementSyntax, attribute: Attribute? = nil) + indirect case ifConfig([IfConfig]) enum Attribute { case ephemeral case ignored } + + struct IfConfig { + let poundKeyword: TokenSyntax + let condition: ExprSyntax? + let cases: [ReducerCase] + } + + enum Body { + case erased + case scoped([String]) + + mutating func append(_ other: Body) { + switch (self, other) { + case let (.scoped(lhs), .scoped(rhs)): + self = .scoped(lhs + rhs) + case (.erased, _): + break + case (_, .erased): + self = .erased + } + } + } + + var stateCaseDecl: String { + switch self { + case let .element(element, attribute): + if attribute != .ignored, + let parameterClause = element.parameterClause, + parameterClause.parameters.count == 1, + let parameter = parameterClause.parameters.first, + parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self) + { + let stateCase = attribute == .ephemeral ? element : element.suffixed("State") + return "case \(stateCase.trimmedDescription)" + } else { + return "case \(element.trimmedDescription)" + } + + case let .ifConfig(configs): + return configs + .map { + """ + \($0.poundKeyword.text) \($0.condition?.trimmedDescription ?? "") + \($0.cases.map(\.stateCaseDecl).joined(separator: "\n")) + """ + } + .joined(separator: "\n") + "#endif\n" + } + } + + var actionCaseDecl: String? { + switch self { + case let .element(element, attribute): + if attribute != .ignored, + let parameterClause = element.parameterClause, + parameterClause.parameters.count == 1, + let parameter = parameterClause.parameters.first, + parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self) + { + return "case \(element.suffixed("Action").trimmedDescription)" + } else { + return nil + } + + case let .ifConfig(configs): + return configs + .map { + let actionCaseDecls = $0.cases.compactMap(\.actionCaseDecl) + return """ + \($0.poundKeyword.text) \($0.condition?.trimmedDescription ?? "") + \(actionCaseDecls.joined(separator: "\n")) + """ + } + .joined(separator: "\n") + "#endif\n" + } + } + + var reducerTypeScope: Body { + switch self { + case let .element(element, attribute): + if attribute == nil, + let parameterClause = element.parameterClause, + parameterClause.parameters.count == 1, + let parameter = parameterClause.parameters.first, + parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self) + { + let type = parameter.type + return .scoped(["ComposableArchitecture.Scope"]) + } else { + return .scoped([]) + } + case .ifConfig: + return .erased + } + } + + var reducerScope: String? { + switch self { + case let .element(element, attribute): + if attribute == nil, + let parameterClause = element.parameterClause, + parameterClause.parameters.count == 1, + let parameter = parameterClause.parameters.first, + parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self) + { + let name = element.name.text + let type = parameter.type + return """ + ComposableArchitecture.Scope(\ + state: \\Self.State.Cases.\(name), action: \\Self.Action.Cases.\(name)\ + ) { + \(type.trimmed)() + } + """ + } else { + return nil + } + case let .ifConfig(configs): + return configs + .map { + let reduceScopes = $0.cases.compactMap(\.reducerScope) + return """ + \($0.poundKeyword.text) \($0.condition?.trimmedDescription ?? "") + \(reduceScopes.joined(separator: "\n")) + + """ + } + .joined() + "#endif\n" + } + } + + var storeCase: String { + switch self { + case let .element(element, attribute): + if attribute == nil, + let parameterClause = element.parameterClause, + parameterClause.parameters.count == 1, + let parameter = parameterClause.parameters.first, + parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self) + { + let name = element.name.text + let type = parameter.type + return "case \(name)(ComposableArchitecture.StoreOf<\(type.trimmed)>)" + } else { + return "case \(element.trimmedDescription)" + } + case let .ifConfig(configs): + return configs + .map { + """ + \($0.poundKeyword.text) \($0.condition?.trimmedDescription ?? "") + \($0.cases.map(\.storeCase).joined(separator: "\n")) + """ + } + .joined(separator: "\n") + "#endif\n" + } + } + + var storeScope: String { + switch self { + case let .element(element, attribute): + let name = element.name.text + if attribute == nil, + let parameterClause = element.parameterClause, + parameterClause.parameters.count == 1, + let parameter = parameterClause.parameters.first, + parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self) + { + return """ + case .\(name): + return .\(name)(store.scope(state: \\.\(name), action: \\.\(name))!) + """ + } else if let parameters = element.parameterClause?.parameters { + let bindingNames = (0.. [ReducerCase.IfConfig] in + guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) + else { return [] } + return [ + ReducerCase.IfConfig( + poundKeyword: decl.poundKeyword, + condition: decl.condition, + cases: Array(members: elements) + ) + ] + } + return [.ifConfig(configs)] + } + return [] + } + } } extension Array where Element == String { diff --git a/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift b/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift index 045c86d8ff6b..bc216685e555 100644 --- a/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift +++ b/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift @@ -550,5 +550,92 @@ """ } } + + func testObservableState_Enum_IfConfig() { + assertMacro { + """ + @ObservableState + public enum State { + case child(ChildFeature.State) + #if os(macOS) + case mac(MacFeature.State) + #elseif os(tvOS) + case tv(TVFeature.State) + #endif + + #if DEBUG + #if INNER + case inner(InnerFeature.State) + #endif + #endif + } + """ + } expansion: { + """ + public enum State { + case child(ChildFeature.State) + #if os(macOS) + case mac(MacFeature.State) + #elseif os(tvOS) + case tv(TVFeature.State) + #endif + + #if DEBUG + #if INNER + case inner(InnerFeature.State) + #endif + #endif + + public var _$id: ComposableArchitecture.ObservableStateID { + switch self { + case let .child(state): + return ._$id(for: state)._$tag(0) + #if os(macOS) + case let .mac(state): + return ._$id(for: state)._$tag(1) + #elseif os(tvOS) + case let .tv(state): + return ._$id(for: state)._$tag(2) + #endif + + #if DEBUG + #if INNER + case let .inner(state): + return ._$id(for: state)._$tag(3) + #endif + #endif + + } + } + + public mutating func _$willModify() { + switch self { + case var .child(state): + ComposableArchitecture._$willModify(&state) + self = .child(state) + #if os(macOS) + case var .mac(state): + ComposableArchitecture._$willModify(&state) + self = .mac(state) + #elseif os(tvOS) + case var .tv(state): + ComposableArchitecture._$willModify(&state) + self = .tv(state) + #endif + + #if DEBUG + #if INNER + case var .inner(state): + ComposableArchitecture._$willModify(&state) + self = .inner(state) + #endif + #endif + + } + } + } + """ + } + } } #endif diff --git a/Tests/ComposableArchitectureMacrosTests/ReducerMacroTests.swift b/Tests/ComposableArchitectureMacrosTests/ReducerMacroTests.swift index 77f00a32386d..89b880a0a6e1 100644 --- a/Tests/ComposableArchitectureMacrosTests/ReducerMacroTests.swift +++ b/Tests/ComposableArchitectureMacrosTests/ReducerMacroTests.swift @@ -6,7 +6,7 @@ final class ReducerMacroTests: XCTestCase { override func invokeTest() { withMacroTesting( - //isRecording: true, + // isRecording: true, macros: [ReducerMacro.self] ) { super.invokeTest() @@ -235,9 +235,7 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.EmptyReducer { - - ComposableArchitecture.EmptyReducer() - + ComposableArchitecture.EmptyReducer() } enum CaseScope { @@ -305,17 +303,15 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.ReducerBuilder._Sequence._Sequence, ComposableArchitecture.Scope>, ComposableArchitecture.Scope> { - - ComposableArchitecture.Scope(state: \Self.State.Cases.activity, action: \Self.Action.Cases.activity) { - Activity() - } - ComposableArchitecture.Scope(state: \Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) { - Timeline() - } - ComposableArchitecture.Scope(state: \Self.State.Cases.tweet, action: \Self.Action.Cases.tweet) { - Tweet() - } - + ComposableArchitecture.Scope(state: \Self.State.Cases.activity, action: \Self.Action.Cases.activity) { + Activity() + } + ComposableArchitecture.Scope(state: \Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) { + Timeline() + } + ComposableArchitecture.Scope(state: \Self.State.Cases.tweet, action: \Self.Action.Cases.tweet) { + Tweet() + } } enum CaseScope { @@ -371,9 +367,7 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.EmptyReducer { - - ComposableArchitecture.EmptyReducer() - + ComposableArchitecture.EmptyReducer() } enum CaseScope { @@ -422,9 +416,7 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.EmptyReducer { - - ComposableArchitecture.EmptyReducer() - + ComposableArchitecture.EmptyReducer() } enum CaseScope { @@ -477,14 +469,12 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.ReducerBuilder._Sequence, ComposableArchitecture.Scope> { - - ComposableArchitecture.Scope(state: \Self.State.Cases.activity, action: \Self.Action.Cases.activity) { - Activity() - } - ComposableArchitecture.Scope(state: \Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) { - Timeline() - } - + ComposableArchitecture.Scope(state: \Self.State.Cases.activity, action: \Self.Action.Cases.activity) { + Activity() + } + ComposableArchitecture.Scope(state: \Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) { + Timeline() + } } enum CaseScope { @@ -541,11 +531,9 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.Scope { - - ComposableArchitecture.Scope(state: \Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) { - Timeline() - } - + ComposableArchitecture.Scope(state: \Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) { + Timeline() + } } enum CaseScope { @@ -607,9 +595,7 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.EmptyReducer { - - ComposableArchitecture.EmptyReducer() - + ComposableArchitecture.EmptyReducer() } enum CaseScope { @@ -672,17 +658,15 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.ReducerBuilder._Sequence._Sequence, ComposableArchitecture.Scope>, ComposableArchitecture.Scope> { - - ComposableArchitecture.Scope(state: \Self.State.Cases.drillDown, action: \Self.Action.Cases.drillDown) { - Counter() - } - ComposableArchitecture.Scope(state: \Self.State.Cases.popover, action: \Self.Action.Cases.popover) { - Counter() - } - ComposableArchitecture.Scope(state: \Self.State.Cases.sheet, action: \Self.Action.Cases.sheet) { - Counter() - } - + ComposableArchitecture.Scope(state: \Self.State.Cases.drillDown, action: \Self.Action.Cases.drillDown) { + Counter() + } + ComposableArchitecture.Scope(state: \Self.State.Cases.popover, action: \Self.Action.Cases.popover) { + Counter() + } + ComposableArchitecture.Scope(state: \Self.State.Cases.sheet, action: \Self.Action.Cases.sheet) { + Counter() + } } enum CaseScope { @@ -737,11 +721,9 @@ @ComposableArchitecture.ReducerBuilder static var body: ComposableArchitecture.Scope { - - ComposableArchitecture.Scope(state: \Self.State.Cases.feature, action: \Self.Action.Cases.feature) { - Nested.Feature() - } - + ComposableArchitecture.Scope(state: \Self.State.Cases.feature, action: \Self.Action.Cases.feature) { + Nested.Feature() + } } enum CaseScope { @@ -960,5 +942,191 @@ """ } } + + func testEnum_IfConfig() { + assertMacro { + """ + @Reducer + enum Feature { + case child(ChildFeature) + + #if os(macOS) + case mac(MacFeature) + case macAlert(AlertState) + #elseif os(iOS) + case phone(PhoneFeature) + #else + case other(OtherFeature) + case another + #endif + + #if DEBUG + #if INNER + case inner(InnerFeature) + case innerDialog(ConfirmationDialogState) + #endif + #endif + } + """ + } expansion: { + #""" + enum Feature { + case child(ChildFeature) + + #if os(macOS) + case mac(MacFeature) + case macAlert(AlertState) + #elseif os(iOS) + case phone(PhoneFeature) + #else + case other(OtherFeature) + case another + #endif + + #if DEBUG + #if INNER + case inner(InnerFeature) + case innerDialog(ConfirmationDialogState) + #endif + #endif + + @CasePathable + @dynamicMemberLookup + @ObservableState + enum State: ComposableArchitecture.CaseReducerState { + typealias StateReducer = Feature + case child(ChildFeature.State) + #if os(macOS) + case mac(MacFeature.State) + case macAlert(AlertState) + #elseif os(iOS) + case phone(PhoneFeature.State) + #else + case other(OtherFeature.State) + case another + #endif + + #if DEBUG + #if INNER + case inner(InnerFeature.State) + case innerDialog(ConfirmationDialogState) + #endif + #endif + + } + + @CasePathable + enum Action { + case child(ChildFeature.Action) + #if os(macOS) + case mac(MacFeature.Action) + case macAlert(AlertState.Action) + #elseif os(iOS) + case phone(PhoneFeature.Action) + #else + case other(OtherFeature.Action) + #endif + + #if DEBUG + #if INNER + case inner(InnerFeature.Action) + case innerDialog(ConfirmationDialogState.Action) + #endif + #endif + + } + + @ComposableArchitecture.ReducerBuilder + static var body: Reduce { + ComposableArchitecture.Reduce( + ComposableArchitecture.CombineReducers { + ComposableArchitecture.Scope(state: \Self.State.Cases.child, action: \Self.Action.Cases.child) { + ChildFeature() + } + #if os(macOS) + ComposableArchitecture.Scope(state: \Self.State.Cases.mac, action: \Self.Action.Cases.mac) { + MacFeature() + } + #elseif os(iOS) + ComposableArchitecture.Scope(state: \Self.State.Cases.phone, action: \Self.Action.Cases.phone) { + PhoneFeature() + } + #else + ComposableArchitecture.Scope(state: \Self.State.Cases.other, action: \Self.Action.Cases.other) { + OtherFeature() + } + #endif + + #if DEBUG + #if INNER + ComposableArchitecture.Scope(state: \Self.State.Cases.inner, action: \Self.Action.Cases.inner) { + InnerFeature() + } + #endif + + #endif + + } + ) + } + + enum CaseScope { + case child(ComposableArchitecture.StoreOf) + #if os(macOS) + case mac(ComposableArchitecture.StoreOf) + case macAlert(AlertState) + #elseif os(iOS) + case phone(ComposableArchitecture.StoreOf) + #else + case other(ComposableArchitecture.StoreOf) + case another + #endif + + #if DEBUG + #if INNER + case inner(ComposableArchitecture.StoreOf) + case innerDialog(ConfirmationDialogState) + #endif + #endif + + } + + static func scope(_ store: ComposableArchitecture.Store) -> CaseScope { + switch store.state { + case .child: + return .child(store.scope(state: \.child, action: \.child)!) + #if os(macOS) + case .mac: + return .mac(store.scope(state: \.mac, action: \.mac)!) + case let .macAlert(v0): + return .macAlert(v0) + #elseif os(iOS) + case .phone: + return .phone(store.scope(state: \.phone, action: \.phone)!) + #else + case .other: + return .other(store.scope(state: \.other, action: \.other)!) + case .another: + return .another + #endif + + #if DEBUG + #if INNER + case .inner: + return .inner(store.scope(state: \.inner, action: \.inner)!) + case let .innerDialog(v0): + return .innerDialog(v0) + #endif + #endif + + } + } + } + + extension Feature: ComposableArchitecture.CaseReducer, ComposableArchitecture.Reducer { + } + """# + } + } } #endif diff --git a/Tests/ComposableArchitectureTests/EnumReducerMacroTests.swift b/Tests/ComposableArchitectureTests/EnumReducerMacroTests.swift new file mode 100644 index 000000000000..0fae5a9ced3e --- /dev/null +++ b/Tests/ComposableArchitectureTests/EnumReducerMacroTests.swift @@ -0,0 +1,31 @@ +#if swift(>=5.9) + import ComposableArchitecture + + private enum TestEnumReducer_CompilerDirective { + @Reducer + struct ChildFeature {} + enum Options {} + + @Reducer + enum Feature { + case child(ChildFeature) + + #if os(macOS) + case mac(ChildFeature) + case macAlert(AlertState) + #elseif os(iOS) + case phone(ChildFeature) + #else + case other(ChildFeature) + case another + #endif + + #if DEBUG + #if INNER + case inner(ChildFeature) + case innerDialog(ConfirmationDialogState) + #endif + #endif + } + } +#endif diff --git a/Tests/ComposableArchitectureTests/MacroTests.swift b/Tests/ComposableArchitectureTests/MacroTests.swift index 406e67658622..d26541e8cb05 100644 --- a/Tests/ComposableArchitectureTests/MacroTests.swift +++ b/Tests/ComposableArchitectureTests/MacroTests.swift @@ -55,7 +55,7 @@ } } - enum TestEnumReducer_Basics { + private enum TestEnumReducer_Basics { @Reducer struct Feature {} @Reducer enum Destination1 { @@ -87,7 +87,7 @@ public enum Destination6 {} } - enum TestEnumReducer_SynthesizedConformances { + private enum TestEnumReducer_SynthesizedConformances { @Reducer struct Feature { } diff --git a/Tests/ComposableArchitectureTests/ObservableStateEnumMacroTests.swift b/Tests/ComposableArchitectureTests/ObservableStateEnumMacroTests.swift new file mode 100644 index 000000000000..0b84b5e30fad --- /dev/null +++ b/Tests/ComposableArchitectureTests/ObservableStateEnumMacroTests.swift @@ -0,0 +1,22 @@ +#if swift(>=5.9) + import ComposableArchitecture + + private enum TestObservableEnum_CompilerDirective { + @Reducer + struct ChildFeature {} + @ObservableState + public enum State { + case child(ChildFeature.State) + #if os(macOS) + case mac(ChildFeature.State) + #elseif os(tvOS) + case tv(ChildFeature.State) + #endif + #if DEBUG + #if INNER + case inner(ChildFeature.State) + #endif + #endif + } + } +#endif