Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions Sources/StateStruct/Source.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@ public macro TrackingIgnored() =
accessor,
names: named(init), named(_read), named(set), named(_modify)
)
@attached(peer, names: prefixed(`_backing_`), prefixed(`$`))
@attached(peer, names: prefixed(`_backing_`), prefixed(`$`))
public macro COWTrackingProperty() =
#externalMacro(module: "StateStructMacros", type: "COWTrackingPropertyMacro")

@attached(
accessor,
names: named(init), named(_read), named(set), named(_modify)
)
@attached(peer, names: prefixed(`_backing_`))
public macro WeakTrackingProperty() =
#externalMacro(module: "StateStructMacros", type: "WeakTrackingPropertyMacro")

#if DEBUG

@Tracking
Expand Down Expand Up @@ -57,10 +65,10 @@ public macro COWTrackingProperty() =
var count: Int = 0
}

@Tracking
struct HashableState: Hashable {
var count: Int = 0
}
@Tracking
struct HashableState: Hashable {
var count: Int = 0
}

@Tracking
struct MyState {
Expand All @@ -72,7 +80,7 @@ struct HashableState: Hashable {
var stored_1: Int = 18

var stored_2: Int

var stored_3: Int = 10 {
didSet {
print("stored_3 did set")
Expand All @@ -95,13 +103,19 @@ struct HashableState: Hashable {
var computed_1: Int {
stored_1
}

weak var weak_stored: Ref?

init() {

}

}

class Ref {

}

#if canImport(Observation)
import Observation

Expand All @@ -114,7 +128,9 @@ struct HashableState: Hashable {
var stored_2: Int

var stored_3: Int?


weak var weak_stored: Hoge?

var stored_4: Int = 10 {
didSet {
print("stored_4 did set")
Expand Down
1 change: 1 addition & 0 deletions Sources/StateStructMacros/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct Plugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
TrackingMacro.self,
COWTrackingPropertyMacro.self,
WeakTrackingPropertyMacro.self,
TrackingIgnoredMacro.self,
]
}
21 changes: 13 additions & 8 deletions Sources/StateStructMacros/TrackingMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,20 @@ extension TrackingMacro: MemberAttributeMacro {
}
}
}

if variableDecl.bindingSpecifier.tokenKind == .keyword(.var) {
let macroAttribute = "@COWTrackingProperty"
let attributeSyntax = AttributeSyntax.init(stringLiteral: macroAttribute)

return [attributeSyntax]

guard variableDecl.bindingSpecifier.tokenKind == .keyword(.var) else {
return []
}

let isWeak = variableDecl.modifiers.contains { modifier in
modifier.name.tokenKind == .keyword(.weak)
}

if isWeak {
return [AttributeSyntax(stringLiteral: "@WeakTrackingProperty")]
} else {
return [AttributeSyntax(stringLiteral: "@COWTrackingProperty")]
}

return []
}

}
148 changes: 148 additions & 0 deletions Sources/StateStructMacros/WeakTrackingPropertyMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacroExpansion
import SwiftSyntaxMacros

public struct WeakTrackingPropertyMacro {

public enum Error: Swift.Error {
case needsTypeAnnotation
}

}

extension WeakTrackingPropertyMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {

guard let variableDecl = declaration.as(VariableDeclSyntax.self) else {
return []
}

guard variableDecl.typeSyntax != nil else {
context.addDiagnostics(from: Error.needsTypeAnnotation, node: declaration)
return []
}

var newMembers: [DeclSyntax] = []

let ignoreMacroAttached = variableDecl.attributes.contains {
switch $0 {
case .attribute(let attribute):
return attribute.attributeName.description == "TrackingIgnored"
case .ifConfigDecl:
return false
}
}

guard !ignoreMacroAttached else {
return []
}

for binding in variableDecl.bindings {
if binding.accessorBlock != nil {
// skip computed properties
continue
}
}

var _variableDecl = variableDecl.trimmed
_variableDecl.attributes = [.init(.init(stringLiteral: "@TrackingIgnored"))]

_variableDecl = _variableDecl.renamingIdentifier(with: "_backing_")

newMembers.append(DeclSyntax(_variableDecl))

return newMembers
}
}

extension WeakTrackingPropertyMacro: AccessorMacro {
public static func expansion(
of node: SwiftSyntax.AttributeSyntax,
providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.AccessorDeclSyntax] {

guard let variableDecl = declaration.as(VariableDeclSyntax.self) else {
return []
}

guard let binding = variableDecl.bindings.first,
let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self)
else {
return []
}

let isConstant = variableDecl.bindingSpecifier.tokenKind == .keyword(.let)
let propertyName = identifierPattern.identifier.text
let backingName = "_backing_" + propertyName
let hasWillSet = variableDecl.willSetBlock != nil

let initAccessor = AccessorDeclSyntax(
"""
@storageRestrictions(initializes: \(raw: backingName))
init(initialValue) {
\(raw: backingName) = initialValue
}
"""
)

let readAccessor = AccessorDeclSyntax(
"""
_read {
_Tracking._tracking_modifyStorage {
$0.accessorRead(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
}
yield \(raw: backingName)
}
"""
)

let setAccessor = AccessorDeclSyntax(
"""
set {
_Tracking._tracking_modifyStorage {
$0.accessorSet(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
}
\(raw: backingName) = newValue
}
"""
)

let modifyAccessor = AccessorDeclSyntax(
"""
_modify {
_Tracking._tracking_modifyStorage {
$0.accessorModify(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
}
yield &\(raw: backingName)
}
"""
)

var accessors: [AccessorDeclSyntax] = []

if binding.initializer == nil {
accessors.append(initAccessor)
}

accessors.append(readAccessor)

if !isConstant {
accessors.append(setAccessor)

if hasWillSet == false {
accessors.append(modifyAccessor)
}
}

return accessors

}

}
69 changes: 49 additions & 20 deletions Tests/StateStructMacroTests/TrackingMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@ final class TrackingMacroTests: XCTestCase {
super.invokeTest()
}
}

func test_ignore_computed() {

assertMacro {
"""
@Tracking
struct MyState {

var c_0: Int {
0
}

var c_1: Int {
get { 0 }
}

var c_2: Int {
get { 0 }
set { }
Expand Down Expand Up @@ -62,16 +62,16 @@ final class TrackingMacroTests: XCTestCase {
}
"""
}

}

func test_stored_observer() {

assertMacro {
"""
@Tracking
struct MyState {

var stored_0: Int = 18 {
didSet {
print("stored_0 did set")
Expand Down Expand Up @@ -99,30 +99,30 @@ final class TrackingMacroTests: XCTestCase {
}
"""
}

}

func test_public() {
assertMacro {
"""
@Tracking
public struct MyState {

private var stored_0: Int = 18

var stored_1: String

let stored_2: Int = 0

var age: Int { 0 }

var age2: Int {
get { 0 }
set { }
}

var height: Int

func compute() {
}
}
Expand Down Expand Up @@ -167,11 +167,11 @@ final class TrackingMacroTests: XCTestCase {
"""
@Tracking
struct MyState {

private var stored_0: Int = 18

var stored_1: String

let stored_2: Int = 0

var age: Int { 0 }
Expand Down Expand Up @@ -221,4 +221,33 @@ final class TrackingMacroTests: XCTestCase {
}

}

func test_weak_property() {

assertMacro {
"""
@Tracking
struct MyState {

weak var weak_stored: Ref?

}
"""
} expansion: {
"""
struct MyState {
@WeakTrackingProperty

weak var weak_stored: Ref?

internal let _tracking_context: _TrackingContext = .init()

}

extension MyState: TrackingObject {
}
"""
}

}
}
Loading