Skip to content

Commit

Permalink
Support package access modifier for Observation architecture (#2741)
Browse files Browse the repository at this point in the history
  • Loading branch information
nnsnodnb committed Jan 30, 2024
1 parent 63e29b3 commit 36e5420
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 ?? [] }
Expand Down
14 changes: 10 additions & 4 deletions Sources/ComposableArchitectureMacros/ViewActionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
#"""
Expand Down Expand Up @@ -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() {
Expand Down
39 changes: 38 additions & 1 deletion Tests/ComposableArchitectureMacrosTests/PresentsMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
}
}

func testPublicAccess() {
func testAccessControl() {
assertMacro {
"""
public struct State {
Expand Down Expand Up @@ -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<Destination.State> {
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<Destination.State>(wrappedValue: nil)
}
"""#
}
}

func testObservableStateDiagnostic() {
Expand Down
48 changes: 48 additions & 0 deletions Tests/ComposableArchitectureMacrosTests/ViewActionMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Feature>
package var body: some View {
EmptyView()
}
}
"""
} expansion: {
"""
package struct FeatureView: View {
package let store: StoreOf<Feature>
package var body: some View {
EmptyView()
}
}
extension FeatureView: ComposableArchitecture.ViewActionSending {
}
"""
}
}

func testWarning_StoreSend() {
assertMacro {
"""
Expand Down

0 comments on commit 36e5420

Please sign in to comment.