Skip to content

Commit ac32f69

Browse files
authored
Fix missing access modifier on CaseScope.AllCasePaths properties (#3898)
When using @Reducer on a public or package enum, the generated computed properties inside AllCasePaths were missing the access modifier, defaulting to internal. This made the enum scope API inaccessible across module boundaries. Fixes #3893
1 parent 76be91a commit ac32f69

File tree

2 files changed

+108
-6
lines changed

2 files changed

+108
-6
lines changed

Sources/ComposableArchitectureMacros/ReducerMacro.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ extension ReducerMacro: MemberMacro {
268268
}
269269
storeCases.append(enumCaseElement.storeCase)
270270
storeScopes.append(enumCaseElement.storeScope)
271-
storeCasePathProperties.append(enumCaseElement.storeCasePathProperty)
271+
storeCasePathProperties.append(enumCaseElement.storeCasePathProperty(access: access))
272272
}
273273
if !hasState {
274274
var conformances: [String] = []
@@ -526,7 +526,8 @@ private enum ReducerCase {
526526
}
527527
}
528528

529-
var storeCasePathProperty: String {
529+
func storeCasePathProperty(access: DeclModifierSyntax?) -> String {
530+
let accessPrefix = access?.name.text.appending(" ") ?? ""
530531
switch self {
531532
case .element(let element, let attribute):
532533
let name = element.name.text
@@ -538,7 +539,7 @@ private enum ReducerCase {
538539
{
539540
let type = parameter.type
540541
return """
541-
var \(name): CasePaths.AnyCasePath<CaseScope, ComposableArchitecture.StoreOf<\(type.trimmed)>> {
542+
\(accessPrefix)var \(name): CasePaths.AnyCasePath<CaseScope, ComposableArchitecture.StoreOf<\(type.trimmed)>> {
542543
CasePaths.AnyCasePath(
543544
embed: CaseScope.\(name),
544545
extract: { guard case let .\(name)(v0) = $0 else { return nil }; return v0 }
@@ -550,7 +551,7 @@ private enum ReducerCase {
550551
let parameter = parameterClause.parameters.first
551552
{
552553
return """
553-
var \(name): CasePaths.AnyCasePath<CaseScope, \(parameter.type.trimmed)> {
554+
\(accessPrefix)var \(name): CasePaths.AnyCasePath<CaseScope, \(parameter.type.trimmed)> {
554555
CasePaths.AnyCasePath(
555556
embed: CaseScope.\(name),
556557
extract: { guard case let .\(name)(v0) = $0 else { return nil }; return v0 }
@@ -561,7 +562,7 @@ private enum ReducerCase {
561562
return ""
562563
} else {
563564
return """
564-
var \(name): CasePaths.AnyCasePath<CaseScope, Void> {
565+
\(accessPrefix)var \(name): CasePaths.AnyCasePath<CaseScope, Void> {
565566
CasePaths.AnyCasePath(
566567
embed: { CaseScope.\(name) },
567568
extract: { guard case .\(name) = $0 else { return nil }; return () }
@@ -570,7 +571,7 @@ private enum ReducerCase {
570571
"""
571572
}
572573
case .ifConfig(let configs):
573-
return Self.renderedIfConfig(configs) { $0.storeCasePathProperty } ?? ""
574+
return Self.renderedIfConfig(configs) { $0.storeCasePathProperty(access: access) } ?? ""
574575
}
575576
}
576577

Tests/ComposableArchitectureMacrosTests/ReducerMacroTests.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,107 @@
855855
}
856856
}
857857

858+
func testEnum_TwoCases_AccessControl_Public() {
859+
assertMacro {
860+
"""
861+
@Reducer
862+
public enum Destination {
863+
case activity(Activity)
864+
case timeline(Timeline)
865+
}
866+
"""
867+
} expansion: {
868+
#"""
869+
public enum Destination {
870+
case activity(Activity)
871+
case timeline(Timeline)
872+
873+
@CasePathable
874+
@dynamicMemberLookup
875+
@ObservableState
876+
877+
public enum State: ComposableArchitecture.CaseReducerState {
878+
879+
public typealias StateReducer = Destination
880+
case activity(Activity.State)
881+
case timeline(Timeline.State)
882+
}
883+
884+
@CasePathable
885+
886+
public enum Action {
887+
case activity(Activity.Action)
888+
case timeline(Timeline.Action)
889+
}
890+
891+
@ComposableArchitecture.ReducerBuilder<Self.State, Self.Action>
892+
893+
public static var body: Reduce<Self.State, Self.Action> {
894+
ComposableArchitecture.Reduce(
895+
ComposableArchitecture.EmptyReducer<Self.State, Self.Action>()
896+
.ifCaseLet(\Self.State.Cases.activity, action: \Self.Action.Cases.activity) {
897+
Activity()
898+
}
899+
.ifCaseLet(\Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) {
900+
Timeline()
901+
}
902+
)
903+
}
904+
905+
@dynamicMemberLookup
906+
907+
public enum CaseScope: ComposableArchitecture._CaseScopeProtocol, CasePaths.CasePathable {
908+
case activity(ComposableArchitecture.StoreOf<Activity>)
909+
case timeline(ComposableArchitecture.StoreOf<Timeline>)
910+
911+
public struct AllCasePaths {
912+
public var activity: CasePaths.AnyCasePath<CaseScope, ComposableArchitecture.StoreOf<Activity>> {
913+
CasePaths.AnyCasePath(
914+
embed: CaseScope.activity,
915+
extract: {
916+
guard case let .activity(v0) = $0 else {
917+
return nil
918+
};
919+
return v0
920+
}
921+
)
922+
}
923+
public var timeline: CasePaths.AnyCasePath<CaseScope, ComposableArchitecture.StoreOf<Timeline>> {
924+
CasePaths.AnyCasePath(
925+
embed: CaseScope.timeline,
926+
extract: {
927+
guard case let .timeline(v0) = $0 else {
928+
return nil
929+
};
930+
return v0
931+
}
932+
)
933+
}
934+
}
935+
936+
public static var allCasePaths: AllCasePaths {
937+
AllCasePaths()
938+
}
939+
}
940+
941+
@preconcurrency @MainActor
942+
943+
public static func scope(_ store: ComposableArchitecture.Store<Self.State, Self.Action>) -> CaseScope {
944+
switch store.state {
945+
case .activity:
946+
return .activity(store.scope(state: \.activity, action: \.activity)!)
947+
case .timeline:
948+
return .timeline(store.scope(state: \.timeline, action: \.timeline)!)
949+
}
950+
}
951+
}
952+
953+
extension Destination: ComposableArchitecture.CaseReducer, ComposableArchitecture.Reducer {
954+
}
955+
"""#
956+
}
957+
}
958+
858959
func testEnum_CaseIgnored() {
859960
assertMacro {
860961
"""

0 commit comments

Comments
 (0)