From 36e54203dd0f00803a704c81ba0b407055e2955d Mon Sep 17 00:00:00 2001 From: Yuya Oka Date: Wed, 31 Jan 2024 04:45:51 +0900 Subject: [PATCH] Support `package` access modifier for Observation architecture (#2741) --- .../ObservableStateMacro.swift | 6 +- .../ViewActionMacro.swift | 14 +- .../ObservableStateMacroTests.swift | 125 ++++++++++++++++++ .../PresentsMacroTests.swift | 39 +++++- .../ViewActionMacroTests.swift | 48 +++++++ 5 files changed, 224 insertions(+), 8 deletions(-) diff --git a/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift b/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift index 5fd3d9cd3fd8..06f575e1f46b 100644 --- a/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift +++ b/Sources/ComposableArchitectureMacros/ObservableStateMacro.swift @@ -134,7 +134,7 @@ extension DeclModifierListSyntax { switch $0.name.tokenKind { case .keyword(let keyword): switch keyword { - case .fileprivate, .private, .internal, .public: + case .fileprivate, .private, .internal, .public, .package: return false default: return true @@ -248,7 +248,7 @@ extension ObservableStateMacro: MemberMacro { var declarations = [DeclSyntax]() - let access = declaration.modifiers.first { $0.name.tokenKind == .keyword(.public) } + 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(access), to: &declarations) @@ -267,7 +267,7 @@ extension ObservableStateMacro { providingMembersOf declaration: Declaration, in context: Context ) throws -> [DeclSyntax] { - let access = declaration.modifiers.first { $0.name.tokenKind == .keyword(.public) } + let access = declaration.modifiers.first { $0.name.tokenKind == .keyword(.public) || $0.name.tokenKind == .keyword(.package) } let enumCaseDecls = declaration.memberBlock.members .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } diff --git a/Sources/ComposableArchitectureMacros/ViewActionMacro.swift b/Sources/ComposableArchitectureMacros/ViewActionMacro.swift index 0b89518daef1..72daf52c230e 100644 --- a/Sources/ComposableArchitectureMacros/ViewActionMacro.swift +++ b/Sources/ComposableArchitectureMacros/ViewActionMacro.swift @@ -26,10 +26,7 @@ public struct ViewActionMacro: ExtensionMacro { leadingTrivia: declarationWithStoreVariable.memberBlock.members.first?.leadingTrivia ?? "\n ", decl: VariableDeclSyntax( - bindingSpecifier: declaration.modifiers - .contains(where: { $0.name.tokenKind == .keyword(.public) }) - ? "public let" - : "let", + bindingSpecifier: declaration.modifiers.bindingSpecifier(), bindings: [ PatternBindingSyntax( pattern: " store" as PatternSyntax, @@ -160,6 +157,15 @@ extension DeclGroupSyntax { } } +extension DeclModifierListSyntax { + fileprivate func bindingSpecifier() -> TokenSyntax { + guard + let modifier = first(where: { $0.name.tokenKind == .keyword(.public) || $0.name.tokenKind == .keyword(.package) }) + else { return "let" } + return "\(raw: modifier.name.text) let" + } +} + extension FunctionCallExprSyntax { fileprivate var sendExpression: ExprSyntax? { guard diff --git a/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift b/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift index abc744a1b381..258de2062833 100644 --- a/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift +++ b/Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift @@ -106,6 +106,95 @@ } } + func testObservableState_AccessControl() throws { + assertMacro { + #""" + @ObservableState + public struct State { + var count = 0 + } + """# + } expansion: { + #""" + public struct State { + var count = 0 { + @storageRestrictions(initializes: _count) + init(initialValue) { + _count = initialValue + } + get { + _$observationRegistrar.access(self, keyPath: \.count) + return _count + } + set { + _$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual) + } + _modify { + let oldValue = _$observationRegistrar.willModify(self, keyPath: \.count, &_count) + defer { + _$observationRegistrar.didModify(self, keyPath: \.count, &_count, oldValue, _$isIdentityEqual) + } + yield &_count + } + } + + var _$observationRegistrar = ComposableArchitecture.ObservationStateRegistrar() + + public var _$id: ComposableArchitecture.ObservableStateID { + _$observationRegistrar.id + } + + public mutating func _$willModify() { + _$observationRegistrar._$willModify() + } + } + """# + } + assertMacro { + #""" + @ObservableState + package struct State { + var count = 0 + } + """# + } expansion: { + #""" + package struct State { + var count = 0 { + @storageRestrictions(initializes: _count) + init(initialValue) { + _count = initialValue + } + get { + _$observationRegistrar.access(self, keyPath: \.count) + return _count + } + set { + _$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual) + } + _modify { + let oldValue = _$observationRegistrar.willModify(self, keyPath: \.count, &_count) + defer { + _$observationRegistrar.didModify(self, keyPath: \.count, &_count, oldValue, _$isIdentityEqual) + } + yield &_count + } + } + + var _$observationRegistrar = ComposableArchitecture.ObservationStateRegistrar() + + package var _$id: ComposableArchitecture.ObservableStateID { + _$observationRegistrar.id + } + + package mutating func _$willModify() { + _$observationRegistrar._$willModify() + } + } + """# + } + } + func testObservableStateIgnored() throws { assertMacro { #""" @@ -242,6 +331,42 @@ } """ } + assertMacro { + """ + @ObservableState + package enum Path { + case feature1(Feature1.State) + case feature2(Feature2.State) + } + """ + } expansion: { + """ + package enum Path { + case feature1(Feature1.State) + case feature2(Feature2.State) + + package var _$id: ComposableArchitecture.ObservableStateID { + switch self { + case let .feature1(state): + return ._$id(for: state)._$tag(0) + case let .feature2(state): + return ._$id(for: state)._$tag(1) + } + } + + package mutating func _$willModify() { + switch self { + case var .feature1(state): + ComposableArchitecture._$willModify(&state) + self = .feature1(state) + case var .feature2(state): + ComposableArchitecture._$willModify(&state) + self = .feature2(state) + } + } + } + """ + } } func testObservableState_Enum_NonObservableCase() { diff --git a/Tests/ComposableArchitectureMacrosTests/PresentsMacroTests.swift b/Tests/ComposableArchitectureMacrosTests/PresentsMacroTests.swift index ec6fe49fd648..fb3540e51321 100644 --- a/Tests/ComposableArchitectureMacrosTests/PresentsMacroTests.swift +++ b/Tests/ComposableArchitectureMacrosTests/PresentsMacroTests.swift @@ -55,7 +55,7 @@ } } - func testPublicAccess() { + func testAccessControl() { assertMacro { """ public struct State { @@ -93,6 +93,43 @@ } """# } + assertMacro { + """ + package struct State { + @Presents package var destination: Destination.State? + } + """ + } expansion: { + #""" + package struct State { + package var destination: Destination.State? { + @storageRestrictions(initializes: _destination) + init(initialValue) { + _destination = PresentationState(wrappedValue: initialValue) + } + get { + _$observationRegistrar.access(self, keyPath: \.destination) + return _destination.wrappedValue + } + set { + _$observationRegistrar.mutate(self, keyPath: \.destination, &_destination.wrappedValue, newValue, _$isIdentityEqual) + } + } + + package var $destination: ComposableArchitecture.PresentationState { + get { + _$observationRegistrar.access(self, keyPath: \.destination) + return _destination.projectedValue + } + set { + _$observationRegistrar.mutate(self, keyPath: \.destination, &_destination.projectedValue, newValue, _$isIdentityEqual) + } + } + + @ObservationStateIgnored private var _destination = ComposableArchitecture.PresentationState(wrappedValue: nil) + } + """# + } } func testObservableStateDiagnostic() { diff --git a/Tests/ComposableArchitectureMacrosTests/ViewActionMacroTests.swift b/Tests/ComposableArchitectureMacrosTests/ViewActionMacroTests.swift index bdbb52af627b..a0425a2aa327 100644 --- a/Tests/ComposableArchitectureMacrosTests/ViewActionMacroTests.swift +++ b/Tests/ComposableArchitectureMacrosTests/ViewActionMacroTests.swift @@ -217,6 +217,54 @@ } } + func testNoStore_Package() { + assertMacro { + """ + @ViewAction(for: Feature.self) + package struct FeatureView: View { + package var body: some View { + EmptyView() + } + } + """ + } diagnostics: { + """ + @ViewAction(for: Feature.self) + ╰─ 🛑 '@ViewAction' requires 'FeatureView' to have a 'store' property of type 'Store'. + ✏️ Add 'store' + package struct FeatureView: View { + package var body: some View { + EmptyView() + } + } + """ + } fixes: { + """ + @ViewAction(for: Feature.self) + package struct FeatureView: View { + package let store: StoreOf + + package var body: some View { + EmptyView() + } + } + """ + } expansion: { + """ + package struct FeatureView: View { + package let store: StoreOf + + package var body: some View { + EmptyView() + } + } + + extension FeatureView: ComposableArchitecture.ViewActionSending { + } + """ + } + } + func testWarning_StoreSend() { assertMacro { """