├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── PropertyTracer-Package.xcscheme │ └── PropertyTracer.xcscheme ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── PropertyTracer │ ├── Macro.swift │ ├── PropertyTracer.swift │ └── exported.swift ├── PropertyTracerPlugin │ ├── Extension │ │ ├── DeclGroupSyntax+.swift │ │ ├── PatternBindingSyntax+.swift │ │ └── VariableDeclSyntax+.swift │ ├── Macro │ │ ├── NoTracedMacro.swift │ │ ├── PropertyTracerMacro.swift │ │ └── _TracedMacro.swift │ ├── PropertyTracerMacroDiagnostic.swift │ └── PropertyTracerMacroPlugin.swift └── PropertyTracerSupport │ ├── Extension │ └── Thread+.swift │ ├── Model │ ├── CallStackInfo.swift │ └── PropertyAccess.swift │ └── util │ ├── demangle.swift │ └── symbolInfo.swift └── Tests └── PropertyTracerTests └── PropertyTracerTests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - README.md 9 | - LICENSE 10 | pull_request: 11 | paths-ignore: 12 | - README.md 13 | - LICENSE 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | 19 | env: 20 | DEVELOPER_DIR: /Applications/Xcode_15.0.app 21 | 22 | jobs: 23 | build: 24 | name: Build & Test 25 | runs-on: macos-13 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Select Xcode 15 31 | run: sudo xcode-select -s /Applications/Xcode_15.0.app 32 | 33 | - name: Build 34 | run: swift build 35 | 36 | - name: Test 37 | run: swift test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/PropertyTracer-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 96 | 98 | 104 | 105 | 106 | 107 | 113 | 114 | 115 | 116 | 122 | 124 | 130 | 131 | 132 | 133 | 135 | 136 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/PropertyTracer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 p-x9 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swiftlang/swift-syntax.git", 7 | "state" : { 8 | "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", 9 | "version" : "510.0.3" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | 6 | let package = Package( 7 | name: "PropertyTracer", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v13), 11 | .tvOS(.v13), 12 | .watchOS(.v6), 13 | .macCatalyst(.v13) 14 | ], 15 | products: [ 16 | .library( 17 | name: "PropertyTracer", 18 | targets: ["PropertyTracer"] 19 | ) 20 | ], 21 | dependencies: [ 22 | .package( 23 | url: "https://github.com/swiftlang/swift-syntax.git", 24 | "509.0.0"..<"511.0.0" 25 | ), 26 | ], 27 | targets: [ 28 | .target( 29 | name: "PropertyTracer", 30 | dependencies: [ 31 | "PropertyTracerSupport", 32 | "PropertyTracerPlugin" 33 | ] 34 | ), 35 | .target( 36 | name: "PropertyTracerSupport" 37 | ), 38 | .macro( 39 | name: "PropertyTracerPlugin", 40 | dependencies: [ 41 | "PropertyTracerSupport", 42 | .product(name: "SwiftSyntax", package: "swift-syntax"), 43 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 44 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 45 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 46 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax") 47 | ] 48 | ), 49 | .testTarget( 50 | name: "PropertyTracerTests", 51 | dependencies: [ 52 | "PropertyTracer", 53 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 54 | ] 55 | ), 56 | ] 57 | ) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PropertyTracer 2 | 3 | Library for tracing access to properties. 4 | 5 | 6 | 7 | [![Github issues](https://img.shields.io/github/issues/p-x9/swift-property-tracer)](https://github.com/p-x9/swift-property-tracer/issues) 8 | [![Github forks](https://img.shields.io/github/forks/p-x9/swift-property-tracer)](https://github.com/p-x9/swift-property-tracer/network/members) 9 | [![Github stars](https://img.shields.io/github/stars/p-x9/swift-property-tracer)](https://github.com/p-x9/swift-property-tracer/stargazers) 10 | [![Github top language](https://img.shields.io/github/languages/top/p-x9/swift-property-tracer)](https://github.com/p-x9/swift-property-tracer/) 11 | 12 | The following information can be obtained: 13 | 14 | - Accessor Type 15 | - get: current value 16 | - set: current & new value 17 | - Call stack info 18 | - caller symbol name 19 | - caller symbol address 20 | - return address 21 | - Parent object 22 | - KeyPath 23 | - ... 24 | 25 | > **Warning** 26 | > It does not work correctly depending on the setting of swift's optimization flag. 27 | > With the default settings, only Debug builds work. 28 | 29 | ## Table of Contents 30 | 31 | - [PropertyTracer](#propertytracer) 32 | - [Table of Contents](#table-of-contents) 33 | - [Usage](#usage) 34 | - [Single Property](#single-property) 35 | - [Additional Info](#additional-info) 36 | - [Get/Set values without tracing](#getset-values-without-tracing) 37 | - [Stop/ReStart tracing a property](#stoprestart-tracing-a-property) 38 | - [Trace all member properties of a certain type](#trace-all-member-properties-of-a-certain-type) 39 | - [License](#license) 40 | 41 | ## Usage 42 | 43 | ### Single Property 44 | 45 | For example, when we define: 46 | 47 | ```swift 48 | struct Item { 49 | @Traced(trace(_:_:)) 50 | var title = "hello" 51 | } 52 | 53 | func trace(_ access: PropertyAccess, _ tracedKeyPath: KeyPath>?) { 54 | print("\n[Access]------------------") 55 | print("\(access.accessor.description)") 56 | print("called from: \(access.callStackInfo.demangledSymbolName ?? "unknown")") 57 | if let parent = access.parent { 58 | print("parent: \(parent)") 59 | } 60 | if let keyPath = access.keyPath { 61 | print("keyPath: \(keyPath)") 62 | } 63 | if let tracedKeyPath { 64 | print("tracedKeyPath: \(tracedKeyPath)") 65 | } 66 | print("----------------------------") 67 | } 68 | ``` 69 | 70 | Suppose the following operation is performed: 71 | 72 | ```swift 73 | let item = Item() 74 | print(item.title) 75 | 76 | item.title = "new value" 77 | 78 | item.printTitle() 79 | ``` 80 | 81 | At this time, the specified `trace` function is called and the output is as follows: 82 | 83 | ```text 84 | [Access]------------------ 85 | getter(String): initial value 86 | called from: PropertyTracerTests.PropertyTracerTests.test() -> () 87 | ---------------------------- 88 | initial value 89 | 90 | [Access]------------------ 91 | setter(String): initial value => new value 92 | called from: PropertyTracerTests.PropertyTracerTests.test() -> () 93 | ---------------------------- 94 | 95 | [Access]------------------ 96 | getter(String): new value 97 | called from: PropertyTracerTests.PropertyTracerTests.Item.printTitle() -> () 98 | ---------------------------- 99 | new value 100 | ``` 101 | 102 | #### Additional Info 103 | 104 | It is also possible to set up additional information to be received in the callback as follows: 105 | 106 | ```swift 107 | struct Item { 108 | // specify parent and variable type 109 | @Traced(trace(_:_:)) 110 | var title = "initial value" 111 | 112 | init(title: String = "initial value") { 113 | self.title = title 114 | 115 | // parent object 116 | _title.parent.value = copiedOwn 117 | 118 | // keyPath 119 | _title.keyPath.value = \Self.title 120 | 121 | // traced keyPath 122 | _title.tracedKeyPath.value = \Self._title 123 | } 124 | 125 | func printTitle() { 126 | print(title) 127 | } 128 | 129 | func copiedOwn() -> Self { 130 | let copied = self 131 | return self 132 | } 133 | } 134 | ``` 135 | 136 | #### Get/Set values without tracing 137 | 138 | For example, what would happen if you accessed the parent property directly in the `trace` function described above? 139 | 140 | Accessing properties within the `trace` function will result in further calls to the `trace` function, leading to an infinite loop. 141 | 142 | Therefore, there are methods that allow manipulation of values without tracing. 143 | 144 | - get value without tracing 145 | 146 | ```swift 147 | let title = item._title.untracedGet() 148 | ``` 149 | 150 | - set value without tracing 151 | 152 | ```swift 153 | let newTitle = "new" 154 | item._title.untracedSet(newTitle) 155 | ``` 156 | 157 | #### Stop/ReStart tracing a property 158 | 159 | - stop tracing 160 | 161 | ```swift 162 | item._title.untraced() 163 | ``` 164 | 165 | - restart taracing 166 | 167 | ```swift 168 | item._title.traced() 169 | ``` 170 | 171 | ### Trace all member properties of a certain type 172 | 173 | The following definition will cause all properties of type Item to be traced. 174 | 175 | It is defined by a macro, and the `parent` and `keyPath` are set automatically. 176 | 177 | Properties to which the `@NoTraced` attribute is attached are excluded from trace. 178 | 179 | ```swift 180 | @PropertyTraced(trace(_:_:)) 181 | class ClassType1 { 182 | static let staticVar = "" 183 | var value1: String = "こんにちは" 184 | var value2: Int = 12 185 | 186 | @NoTraced 187 | var value3: Double = 1.0 188 | 189 | func modify() { 190 | value1 = "hello" 191 | value2 *= 2 192 | value3 = 14 193 | } 194 | } 195 | 196 | func trace(_ access: AnyPropertyAccess, _ tracedKeyPath: AnyKeyPath?) { 197 | print("\n[Access]------------------") 198 | print("\(access.accessor.description)") 199 | print("called from: \(access.callStackInfo.demangledSymbolName ?? "unknown")") 200 | if let parent = access.parent { 201 | print("parent: \(parent)") 202 | } 203 | if let keyPath = access.keyPath { 204 | print("keyPath: \(keyPath)") 205 | } 206 | if let tracedKeyPath { 207 | print("tracedKeyPath: \(tracedKeyPath)") 208 | } 209 | print("----------------------------") 210 | } 211 | ``` 212 | 213 | ## License 214 | 215 | PropertyTracer is released under the MIT License. See [LICENSE](./LICENSE) 216 | -------------------------------------------------------------------------------- /Sources/PropertyTracer/Macro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Macro.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/02. 6 | // 7 | // 8 | 9 | import Foundation 10 | import PropertyTracerSupport 11 | 12 | @attached(memberAttribute) 13 | @attached(member, names: arbitrary) 14 | public macro PropertyTraced( 15 | _ callback: ((AnyPropertyAccess, AnyKeyPath) -> Void)? = nil 16 | ) = #externalMacro( 17 | module: "PropertyTracerPlugin", 18 | type: "PropertyTracerMacro" 19 | ) 20 | 21 | @attached(accessor, names: named(init), named(get), named(set)) 22 | public macro _Traced( 23 | _ callback: ((AnyPropertyAccess, AnyKeyPath) -> Void)? = nil 24 | ) = #externalMacro( 25 | module: "PropertyTracerPlugin", 26 | type: "_TracedMacro" 27 | ) 28 | 29 | @attached(peer) 30 | public macro NoTraced() = #externalMacro( 31 | module: "PropertyTracerPlugin", 32 | type: "NoTracedMacro" 33 | ) 34 | -------------------------------------------------------------------------------- /Sources/PropertyTracer/PropertyTracer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PropertyTracerSupport 3 | 4 | /// A property wrapper for tracing access to a property. 5 | /// 6 | /// This wrapper logs access to a property and can invoke a callback whenever the property is accessed. 7 | /// It captures the call stack at the point of access to provide contextual information. 8 | @propertyWrapper 9 | public struct Traced { 10 | public typealias Callback = (_ access: PropertyAccess, _ tracedKeyPath: TracedKeyPath?) -> Void 11 | public typealias TracedKeyPath = KeyPath> 12 | 13 | @_optimize(none) 14 | public var wrappedValue: V { 15 | get { 16 | if isTraced.value { 17 | let infos = Thread.callStackInfos(addresses: Thread.callStackReturnAddresses) 18 | // [0]: PropertyTracer.Traced.wrappedValue.getter 19 | // [1]: XXXX.getter 20 | 21 | handlePropertyAccess( 22 | accessor: .getter(ref.value), 23 | callStackInfos: infos 24 | ) 25 | } 26 | 27 | return ref.value 28 | } 29 | nonmutating set { 30 | if isTraced.value { 31 | let infos = Thread.callStackInfos(addresses: Thread.callStackReturnAddresses) 32 | // [0]: PropertyTracer.Traced.wrappedValue.setter 33 | // [1]: XXXX.setter 34 | 35 | handlePropertyAccess( 36 | accessor: .setter( 37 | .init(currentValue: ref.value, newValue: newValue) 38 | ), 39 | callStackInfos: infos 40 | ) 41 | } 42 | 43 | ref.value = newValue 44 | } 45 | } 46 | 47 | public var projectedValue: Self { self } 48 | 49 | private let ref: Ref 50 | 51 | /// Parent object 52 | /// 53 | /// Use closure so that the most recent values can be retrieved. 54 | /// Use `Ref` to hold references. 55 | /// 56 | /// Do not make strong references when the parent object's type P is a reference type. 57 | /// ```swift 58 | /// parent.value = { [weak xxx] xxx } 59 | /// ``` 60 | public let parent: Ref<() -> P?> 61 | 62 | /// KeyPath of target property for parent 63 | public let keyPath: Ref?> 64 | 65 | /// KeyPath of `Traced` wrap property of target property for parent. 66 | public let tracedKeyPath: Ref 67 | 68 | /// callback when accessing target property 69 | public var callback: Ref 70 | 71 | private let isTraced: Ref = .init(value: true) 72 | 73 | 74 | /// Creates a new traced property. 75 | /// 76 | /// - Parameters: 77 | /// - wrappedValue: The initial value of the property. 78 | /// - parent: An autoclosure that returns the parent object, if any. 79 | /// - keyPath: An autoclosure that returns the key path to the property, if any. 80 | /// - callback: A callback to invoke whenever the property is accessed. 81 | public init( 82 | wrappedValue: V, 83 | parent: @autoclosure @escaping () -> P? = nil, 84 | keyPath: KeyPath? = nil, 85 | tracedKeyPath: TracedKeyPath? = nil, 86 | _ callback: Callback? = nil 87 | ) { 88 | ref = .init(value: wrappedValue) 89 | self.parent = .init(value: parent) 90 | self.keyPath = .init(value: keyPath) 91 | self.tracedKeyPath = .init(value: tracedKeyPath) 92 | self.callback = .init(value: callback) 93 | } 94 | 95 | /// Handles the property access, logging the access and invoking the callback if set. 96 | /// 97 | /// - Parameters: 98 | /// - accessor: The type of access (getter or setter). 99 | /// - callStackInfos: The call stack at the point of access. 100 | func handlePropertyAccess( 101 | accessor: PropertyAccess.Accessor, 102 | callStackInfos: [CallStackInfo] 103 | ) { 104 | guard callStackInfos.count > 2 else { return } 105 | let info = callStackInfos[2] 106 | 107 | let access = PropertyAccess( 108 | accessor: accessor, 109 | callStackInfo: info, 110 | parent: parent.value(), 111 | keyPath: keyPath.value 112 | ) 113 | 114 | // print("[Access] ", terminator: "") 115 | // if let keyPath = keyPath.value() { 116 | // print("\(keyPath) ", terminator: "") 117 | // } 118 | // print("\(accessor.description)") 119 | // print(" called from:", info.demangledSymbolName ?? "unknown") 120 | // if let parent = parent.value() { 121 | // print(" parent", parent) 122 | // } 123 | 124 | callback.value?(access, tracedKeyPath.value) 125 | } 126 | } 127 | 128 | extension Traced where P: AnyObject { 129 | /// Sets the parent object for weak referencing. 130 | /// 131 | /// - Parameter parent: The parent object. 132 | public func setParent(_ parent: P) { 133 | self.parent.value = { [weak parent] in parent } 134 | } 135 | } 136 | extension Traced { 137 | /// Sets the parent object. 138 | /// 139 | /// - Parameter parent: The parent object. 140 | @_disfavoredOverload 141 | public func setParent(_ parent: P) { 142 | self.parent.value = { parent } 143 | } 144 | 145 | public typealias AnyCallback = (_ access: AnyPropertyAccess, _ tracedKeyPath: AnyKeyPath?) -> Void 146 | 147 | /// Sets the callback to be invoked on property access. 148 | /// 149 | /// - Parameter callBack: The callback. 150 | public func setCallback(_ callBack: AnyCallback?) { 151 | self.callback.value = { 152 | callBack?(AnyPropertyAccess($0), $1) 153 | } 154 | } 155 | } 156 | 157 | /// A reference-holding class. 158 | public class Ref { 159 | 160 | /// The value held by the reference. 161 | public var value: V 162 | 163 | /// Creates a new reference. 164 | /// 165 | /// - Parameter value: The initial value. 166 | public init(value: V) { 167 | self.value = value 168 | } 169 | } 170 | 171 | 172 | extension Traced { 173 | // Creates a new traced property with a callback, without requiring a parent or key path. 174 | /// 175 | /// - Parameters: 176 | /// - wrappedValue: The initial value of the property. 177 | /// - callback: A callback to invoke whenever the property is accessed. 178 | /// 179 | /// Used for global variables, etc. 180 | /// Type P automatically specifies Any. 181 | public init( 182 | wrappedValue: V, 183 | _ callback: Callback? = nil 184 | ) where P == Any { 185 | ref = .init(value: wrappedValue) 186 | self.parent = .init(value: { nil }) 187 | self.keyPath = .init(value: nil) 188 | self.tracedKeyPath = .init(value: nil) 189 | self.callback = .init(value: callback) 190 | } 191 | } 192 | 193 | extension Traced { 194 | /// Enable Tracing 195 | public func traced() { 196 | isTraced.value = true 197 | } 198 | 199 | /// Disable tracing 200 | public func untraced() { 201 | isTraced.value = false 202 | } 203 | } 204 | 205 | extension Traced { 206 | /// Get value without tracing 207 | public func untracedGet() -> V { 208 | let isTraced = isTraced.value 209 | 210 | untraced() 211 | 212 | let value = wrappedValue 213 | 214 | if isTraced { 215 | traced() 216 | } 217 | 218 | return value 219 | } 220 | 221 | /// Set value without tracing 222 | public func untracedSet(_ newValue: V) { 223 | let isTraced = isTraced.value 224 | 225 | untraced() 226 | 227 | wrappedValue = newValue 228 | 229 | if isTraced { 230 | traced() 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /Sources/PropertyTracer/exported.swift: -------------------------------------------------------------------------------- 1 | // 2 | // exported.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/02. 6 | // 7 | // 8 | 9 | @_exported import PropertyTracerSupport 10 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/Extension/DeclGroupSyntax+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclGroupSyntax+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/02. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftSyntax 11 | 12 | extension DeclGroupSyntax { 13 | /// A computed property to get the name of the declaration. 14 | /// 15 | /// The name is extracted from the declaration syntax node, trimmed of whitespaces, and returned as a string. 16 | /// If the declaration is not a class, struct, actor, or enum declaration, `nil` is returned. 17 | var name: String? { 18 | if let decl = self.as(ClassDeclSyntax.self) { 19 | return decl.name.trimmed.text 20 | } else if let decl = self.as(StructDeclSyntax.self) { 21 | return decl.name.trimmed.text 22 | } else if let decl = self.as(ActorDeclSyntax.self) { 23 | return decl.name.trimmed.text 24 | } else if let decl = self.as(EnumDeclSyntax.self) { 25 | return decl.name.trimmed.text 26 | } else { 27 | return nil 28 | } 29 | } 30 | 31 | /// Computed property to check if a declaration is a reference type. 32 | /// 33 | /// Returns true if it is a Class or Actor type. 34 | var isReferenceType: Bool { 35 | if self.as(ClassDeclSyntax.self) == nil || 36 | self.as(ActorDeclSyntax.self) == nil { 37 | return true 38 | } 39 | return false 40 | } 41 | 42 | /// A computed property to check if the declaration is an extension declaration. 43 | var isExtension: Bool { 44 | if self.as(ExtensionDeclSyntax.self) == nil { 45 | return true 46 | } 47 | return false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/Extension/PatternBindingSyntax+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PatternBindingSyntax+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | 13 | extension PatternBindingSyntax { 14 | /// The setter accessor declaration of the pattern binding, if it exists. 15 | /// 16 | /// Assignments can also be used to add a setter to the accessor of pattern bindings. 17 | public var setter: AccessorDeclSyntax? { 18 | get { 19 | guard let accessors = accessorBlock?.accessors, 20 | case let .accessors(list) = accessors else { 21 | return nil 22 | } 23 | return list.first(where: { 24 | $0.accessorSpecifier.tokenKind == .keyword(.set) 25 | }) 26 | } 27 | 28 | set { 29 | // NOTE: Be careful that setter cannot be implemented without a getter. 30 | setNewAccessor(kind: .keyword(.set), newValue: newValue) 31 | } 32 | } 33 | 34 | /// The getter accessor declaration of the pattern binding, if it exists. 35 | /// 36 | /// Assignments can also be used to add a getter to the accessor of pattern bindings. 37 | public var getter: AccessorDeclSyntax? { 38 | get { 39 | switch accessorBlock?.accessors { 40 | case let .accessors(list): 41 | return list.first(where: { 42 | $0.accessorSpecifier.tokenKind == .keyword(.get) 43 | }) 44 | case let .getter(body): 45 | return AccessorDeclSyntax(accessorSpecifier: .keyword(.get), body: .init(statements: body)) 46 | case .none: 47 | return nil 48 | } 49 | } 50 | 51 | set { 52 | let newAccessors: AccessorBlockSyntax.Accessors 53 | 54 | switch accessorBlock?.accessors { 55 | case .getter, .none: 56 | if let newValue { 57 | if let body = newValue.body { 58 | newAccessors = .getter(body.statements) 59 | } else { 60 | let accessors = AccessorDeclListSyntax { 61 | newValue 62 | } 63 | newAccessors = .accessors(accessors) 64 | } 65 | } else { 66 | accessorBlock = .none 67 | return 68 | } 69 | 70 | case let .accessors(list): 71 | var newList = list 72 | let accessor = list.first(where: { accessor in 73 | accessor.accessorSpecifier.tokenKind == .keyword(.get) 74 | }) 75 | if let accessor, 76 | let index = list.index(of: accessor) { 77 | if let newValue { 78 | newList[index] = newValue 79 | } else { 80 | newList.remove(at: index) 81 | } 82 | } else if let newValue { 83 | newList.append(newValue) 84 | } 85 | newAccessors = .accessors(newList) 86 | } 87 | 88 | if accessorBlock == nil { 89 | accessorBlock = .init(accessors: newAccessors) 90 | } else { 91 | accessorBlock = accessorBlock?.with(\.accessors, newAccessors) 92 | } 93 | } 94 | } 95 | 96 | /// A Boolean value indicating whether the pattern binding is get-only property. 97 | public var isGetOnly: Bool { 98 | if initializer != nil { 99 | return false 100 | } 101 | if let accessors = accessorBlock?.accessors, 102 | case let .accessors(list) = accessors, 103 | list.contains(where: { $0.accessorSpecifier.tokenKind == .keyword(.set) }) { 104 | return false 105 | } 106 | if accessorBlock == nil && initializer == nil { 107 | return false 108 | } 109 | return true 110 | } 111 | } 112 | 113 | extension PatternBindingSyntax { 114 | /// The `willSet` accessor declaration of the pattern binding, if it exists. 115 | /// 116 | /// Assignments can also be used to add a wllSet to the accessor of pattern bindings. 117 | public var willSet: AccessorDeclSyntax? { 118 | get { 119 | if let accessors = accessorBlock?.accessors, 120 | case let .accessors(list) = accessors { 121 | return list.first(where: { 122 | $0.accessorSpecifier.tokenKind == .keyword(.willSet) 123 | }) 124 | } 125 | return nil 126 | } 127 | set { 128 | // NOTE: Be careful that willSet cannot be implemented without a setter. 129 | setNewAccessor(kind: .keyword(.willSet), newValue: newValue) 130 | } 131 | } 132 | 133 | /// The `didSet` accessor declaration of the pattern binding, if it exists. 134 | /// 135 | /// Assignments can also be used to add a didSet to the accessor of pattern bindings. 136 | public var didSet: AccessorDeclSyntax? { 137 | get { 138 | if let accessors = accessorBlock?.accessors, 139 | case let .accessors(list) = accessors { 140 | return list.first(where: { 141 | $0.accessorSpecifier.tokenKind == .keyword(.didSet) 142 | }) 143 | } 144 | return nil 145 | } 146 | set { 147 | // NOTE: Be careful that didSet cannot be implemented without a setter. 148 | setNewAccessor(kind: .keyword(.willSet), newValue: newValue) 149 | } 150 | } 151 | } 152 | 153 | extension PatternBindingSyntax { 154 | // NOTE: - getter requires extra steps and should not be used. 155 | private mutating func setNewAccessor(kind: TokenKind, newValue: AccessorDeclSyntax?) { 156 | var newAccessor: AccessorBlockSyntax.Accessors 157 | 158 | switch accessorBlock?.accessors { 159 | case let .getter(body): 160 | guard let newValue else { return } 161 | newAccessor = .accessors( 162 | AccessorDeclListSyntax { 163 | AccessorDeclSyntax(accessorSpecifier: .keyword(.get), body: .init(statements: body)) 164 | newValue 165 | } 166 | ) 167 | case let .accessors(list): 168 | var newList = list 169 | let accessor = list.first(where: { accessor in 170 | accessor.accessorSpecifier.tokenKind == kind 171 | }) 172 | if let accessor, 173 | let index = list.index(of: accessor) { 174 | if let newValue { 175 | newList[index] = newValue 176 | } else { 177 | newList.remove(at: index) 178 | } 179 | } 180 | newAccessor = .accessors(newList) 181 | case .none: 182 | guard let newValue else { return } 183 | newAccessor = .accessors( 184 | AccessorDeclListSyntax { 185 | newValue 186 | } 187 | ) 188 | } 189 | 190 | if accessorBlock == nil { 191 | accessorBlock = .init(accessors: newAccessor) 192 | } else { 193 | accessorBlock = accessorBlock?.with(\.accessors, newAccessor) 194 | } 195 | } 196 | } 197 | 198 | extension PatternBindingSyntax { 199 | /// A Boolean value indicating whether the pattern binding is a stored property. 200 | public var isStored: Bool { 201 | setter == nil && getter == nil 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/Extension/VariableDeclSyntax+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VariableDeclSyntax+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/06/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftSyntax 11 | 12 | extension VariableDeclSyntax { 13 | /// A computed property to check if the variable declaration is a constant (`let`). 14 | /// 15 | /// Returns `true` if the variable declaration uses the `let` keyword, and `false` otherwise. 16 | public var isLet: Bool { 17 | bindingSpecifier.tokenKind == .keyword(.let) 18 | } 19 | 20 | /// A computed property to check if the variable declaration is a variable (`var`). 21 | /// 22 | /// Returns `true` if the variable declaration uses the `var` keyword, and `false` otherwise. 23 | public var isVar: Bool { 24 | bindingSpecifier.tokenKind == .keyword(.var) 25 | } 26 | } 27 | 28 | extension VariableDeclSyntax { 29 | /// A computed property to check if the variable declaration is static. 30 | /// 31 | /// Returns `true` if the variable declaration includes the `static` keyword among its modifiers, and `false` otherwise. 32 | public var isStatic: Bool { 33 | modifiers.contains { modifier in 34 | modifier.name.tokenKind == .keyword(.static) 35 | } 36 | } 37 | 38 | /// A computed property to check if the variable declaration is class-scoped. 39 | /// 40 | /// Returns `true` if the variable declaration includes the `class` keyword among its modifiers, and `false` otherwise. 41 | public var isClass: Bool { 42 | modifiers.contains { modifier in 43 | modifier.name.tokenKind == .keyword(.class) 44 | } 45 | } 46 | 47 | // A computed property to check if the variable declaration is instance-scoped. 48 | /// 49 | /// Returns `true` if the variable declaration is neither static nor class, and `false` otherwise. 50 | public var isInstance: Bool { 51 | !isClass && !isStatic 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/Macro/NoTracedMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoTracedMacro.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/02. 6 | // 7 | // 8 | 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | import SwiftSyntaxMacros 12 | 13 | /// This macro is a mark for members of the type to which the `@PropertyTraced` macro is added that should not be traced 14 | struct NoTracedMacro {} 15 | 16 | extension NoTracedMacro: PeerMacro { 17 | static func expansion( 18 | of node: AttributeSyntax, 19 | providingPeersOf declaration: some DeclSyntaxProtocol, 20 | in context: some MacroExpansionContext 21 | ) throws -> [DeclSyntax] { 22 | guard declaration.as(VariableDeclSyntax.self) != nil else { 23 | context.diagnose( 24 | PropertyTracerMacroDiagnostic.requiresVariableDeclaration.diagnose(at: declaration) 25 | ) 26 | return [] 27 | } 28 | return [] 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/Macro/PropertyTracerMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyTracerMacro.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/01. 6 | // 7 | // 8 | 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | import SwiftSyntaxMacros 12 | 13 | /// This macro is added to the class, struct, and actor declarations. 14 | /// It traces access to the properties to which it belongs. 15 | /// 16 | /// Does not apply to properties for which the `Traced` property wrapper or the `NoTraced` macro is used. 17 | public struct PropertyTracerMacro { 18 | struct Arguments { 19 | let callbackExpr: ExprSyntax 20 | 21 | init( 22 | callbackExpr: ExprSyntax 23 | ) { 24 | self.callbackExpr = callbackExpr 25 | } 26 | } 27 | 28 | static func arguments( 29 | of node: AttributeSyntax, 30 | context: some MacroExpansionContext 31 | ) -> Arguments? { 32 | guard case let .argumentList(arguments) = node.arguments, 33 | let firstElement = arguments.first?.expression else { 34 | return nil 35 | } 36 | 37 | if let closureExpr = firstElement.as(ClosureExprSyntax.self) { 38 | context.diagnose( 39 | PropertyTracerMacroDiagnostic.closureIsNotSupported.diagnose(at: closureExpr) 40 | ) 41 | return nil 42 | } 43 | 44 | return .init( 45 | callbackExpr: firstElement 46 | ) 47 | } 48 | } 49 | 50 | extension PropertyTracerMacro: MemberAttributeMacro { 51 | public static func expansion( 52 | of node: AttributeSyntax, 53 | attachedTo declaration: some DeclGroupSyntax, 54 | providingAttributesFor member: some DeclSyntaxProtocol, 55 | in context: some MacroExpansionContext 56 | ) throws -> [AttributeSyntax] { 57 | guard let variableDecl = member.as(VariableDeclSyntax.self), 58 | variableDecl.isVar, 59 | variableDecl.isInstance, 60 | let binding = variableDecl.bindings.first, 61 | binding.isStored, 62 | binding.typeAnnotation != nil else { 63 | return [] 64 | } 65 | 66 | if variableDecl.attributes.contains(where: { element in 67 | guard case let .attribute(attribute) = element, 68 | let name = attribute.attributeName.as(IdentifierTypeSyntax.self), 69 | ["Traced", "NoTraced"].contains(name.name.trimmed.text) else { 70 | return false 71 | } 72 | return true 73 | }) { 74 | return [] 75 | } 76 | 77 | let argument = arguments(of: node, context: context) 78 | 79 | return [ 80 | AttributeSyntax( 81 | attributeName: IdentifierTypeSyntax( 82 | name: .identifier("_Traced") 83 | ), 84 | leftParen: .leftParenToken(), 85 | arguments: .argumentList( 86 | LabeledExprListSyntax { 87 | if let argument { 88 | LabeledExprSyntax( 89 | expression: argument.callbackExpr 90 | ) 91 | } 92 | } 93 | ), 94 | rightParen: .rightParenToken() 95 | ) 96 | ] 97 | } 98 | } 99 | 100 | extension PropertyTracerMacro: MemberMacro { 101 | public static func expansion( 102 | of node: AttributeSyntax, 103 | providingMembersOf declaration: some DeclGroupSyntax, 104 | in context: some MacroExpansionContext 105 | ) throws -> [DeclSyntax] { 106 | 107 | guard let groupName = declaration.name else { return [] } 108 | 109 | let variables = declaration.memberBlock.members 110 | .lazy 111 | .compactMap { $0.decl.as(VariableDeclSyntax.self) } 112 | .filter { $0.isVar } 113 | .filter { $0.isInstance } 114 | .filter { $0.bindings.count == 1 } 115 | .filter { 116 | !$0.attributes.contains { element in 117 | guard case let .attribute(attribute) = element, 118 | let name = attribute.attributeName.as(IdentifierTypeSyntax.self), 119 | ["Traced", "NoTraced"].contains(name.name.trimmed.text) else { 120 | return false 121 | } 122 | return true 123 | } 124 | } 125 | 126 | let decls = variables 127 | .lazy 128 | .compactMap { variable -> PatternBindingSyntax? in 129 | variable.bindings.first 130 | } 131 | .filter { $0.typeAnnotation != nil } 132 | .filter { $0.isStored } 133 | .map { 134 | DeclSyntax("var _\($0.pattern): Traced<\(raw: groupName),\($0.typeAnnotation!.type.trimmed)>") 135 | } 136 | 137 | 138 | return Array(decls) 139 | } 140 | } 141 | 142 | 143 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/Macro/_TracedMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _TracedMacro.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/02. 6 | // 7 | // 8 | 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | import SwiftSyntaxMacros 12 | 13 | /// This macro is added to the member properties of its type declaration primarily by the `@PropertyAccess` macro. 14 | /// 15 | /// Implement Init, get, set accessors 16 | struct _TracedMacro { 17 | struct Arguments { 18 | let callbackExpr: ExprSyntax 19 | 20 | init( 21 | callbackExpr: ExprSyntax 22 | ) { 23 | self.callbackExpr = callbackExpr 24 | } 25 | } 26 | 27 | static func arguments( 28 | of node: AttributeSyntax, 29 | context: some MacroExpansionContext 30 | ) -> Arguments? { 31 | guard case let .argumentList(arguments) = node.arguments, 32 | let firstElement = arguments.first?.expression else { 33 | return nil 34 | } 35 | 36 | if let closureExpr = firstElement.as(ClosureExprSyntax.self) { 37 | context.diagnose( 38 | PropertyTracerMacroDiagnostic.closureIsNotSupported.diagnose(at: closureExpr) 39 | ) 40 | return nil 41 | } 42 | 43 | return .init( 44 | callbackExpr: firstElement 45 | ) 46 | } 47 | } 48 | 49 | extension _TracedMacro: AccessorMacro { 50 | static func expansion( 51 | of node: AttributeSyntax, 52 | providingAccessorsOf declaration: some DeclSyntaxProtocol, 53 | in context: some MacroExpansionContext 54 | ) throws -> [AccessorDeclSyntax] { 55 | guard let variableDecl = declaration.as(VariableDeclSyntax.self) else { 56 | context.diagnose( 57 | PropertyTracerMacroDiagnostic.requiresVariableDeclaration.diagnose(at: declaration) 58 | ) 59 | return [] 60 | } 61 | 62 | guard variableDecl.isVar else { 63 | context.diagnose( 64 | PropertyTracerMacroDiagnostic.requiredMutableVariableDeclaration.diagnose(at: declaration) 65 | ) 66 | return [] 67 | } 68 | 69 | guard let binding = variableDecl.bindings.first else { 70 | context.diagnose( 71 | PropertyTracerMacroDiagnostic.multipleVariableDeclarationIsNotSupported.diagnose(at: declaration) 72 | ) 73 | return [] 74 | } 75 | 76 | guard binding.isStored else { 77 | context.diagnose( 78 | PropertyTracerMacroDiagnostic.getterAndSetterShouldBeNil.diagnose(at: declaration) 79 | ) 80 | return [] 81 | } 82 | 83 | guard let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.trimmed else { 84 | return [] 85 | } 86 | 87 | guard binding.typeAnnotation?.type.trimmed != nil else { 88 | context.diagnose( 89 | PropertyTracerMacroDiagnostic.specifyTypeExplicitly.diagnose(at: declaration) 90 | ) 91 | return [] 92 | } 93 | 94 | let argument = arguments(of: node, context: context) 95 | 96 | 97 | let setCallback = """ 98 | _\(identifier).setCallback(\(argument?.callbackExpr.description ?? "nil")) 99 | """ 100 | 101 | return [ 102 | """ 103 | @storageRestrictions(initializes: _\(identifier)) 104 | init(initialValue) { 105 | _\(identifier) = Traced(wrappedValue: initialValue, 106 | keyPath: \\.\(identifier)) 107 | } 108 | """, 109 | """ 110 | get { 111 | _\(identifier).setParent(self) 112 | _\(identifier).tracedKeyPath.value = \\Self._\(identifier) 113 | \(raw: setCallback) 114 | return _\(identifier).wrappedValue 115 | } 116 | set { 117 | _\(identifier).setParent(self) 118 | _\(identifier).tracedKeyPath.value = \\Self._\(identifier) 119 | \(raw: setCallback) 120 | _\(identifier).wrappedValue = newValue 121 | } 122 | """ 123 | ] 124 | } 125 | 126 | 127 | } 128 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/PropertyTracerMacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyTracerMacroDiagnostic.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/01. 6 | // 7 | // 8 | 9 | import SwiftSyntax 10 | import SwiftDiagnostics 11 | 12 | enum PropertyTracerMacroDiagnostic { 13 | case requiresVariableDeclaration 14 | case requiredMutableVariableDeclaration 15 | case multipleVariableDeclarationIsNotSupported 16 | case getterAndSetterShouldBeNil 17 | case specifyTypeExplicitly 18 | case closureIsNotSupported 19 | } 20 | 21 | extension PropertyTracerMacroDiagnostic: DiagnosticMessage { 22 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic { 23 | Diagnostic(node: Syntax(node), message: self) 24 | } 25 | 26 | public var message: String { 27 | switch self { 28 | case .requiresVariableDeclaration: 29 | return "This macro must be attached to the property declaration." 30 | case .requiredMutableVariableDeclaration: 31 | return "This macro can only be applied to a 'var'" 32 | case .multipleVariableDeclarationIsNotSupported: 33 | return """ 34 | Multiple variable declaration in one statement is not supported. 35 | """ 36 | case .getterAndSetterShouldBeNil: 37 | return "getter and setter must not be implemented." 38 | case .specifyTypeExplicitly: 39 | return "Specify a type explicitly." 40 | case .closureIsNotSupported: 41 | return """ 42 | Unsupported closure type argument. Please specify the function. 43 | """ 44 | } 45 | } 46 | 47 | public var severity: DiagnosticSeverity { 48 | switch self { 49 | default: 50 | return .error 51 | } 52 | } 53 | 54 | public var diagnosticID: MessageID { 55 | MessageID(domain: "Swift", id: "PropertyTracer.\(self)") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/PropertyTracerPlugin/PropertyTracerMacroPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyTracerMacroPlugin.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/11/01. 6 | // 7 | // 8 | 9 | #if canImport(SwiftCompilerPlugin) 10 | import SwiftSyntaxMacros 11 | import SwiftCompilerPlugin 12 | 13 | @main 14 | struct PropertyTracerMacroPlugin: CompilerPlugin { 15 | let providingMacros: [Macro.Type] = [ 16 | PropertyTracerMacro.self, 17 | _TracedMacro.self, 18 | NoTracedMacro.self 19 | ] 20 | } 21 | #endif 22 | 23 | -------------------------------------------------------------------------------- /Sources/PropertyTracerSupport/Extension/Thread+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Thread+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/10/31. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension Thread { 12 | /// Retrieves the dynamic linking information for a given set of addresses. 13 | /// 14 | /// - Parameter addresses: An array of addresses for which to retrieve dynamic linking information. 15 | /// - Returns: An array of `Dl_info` structures containing the dynamic linking informat 16 | package class func callStackDLInfos(addresses: [NSNumber]) -> [Dl_info] { 17 | addresses.compactMap { symbolInfo(for: $0.uintValue) } 18 | } 19 | 20 | /// Retrieves the call stack information for a given set of addresses. 21 | /// 22 | /// - Parameter addresses: An array of addresses for which to retrieve call stack information. 23 | /// - Returns: An array of `CallStackInfo` structures containing the call stack information for each address. 24 | package class func callStackInfos(addresses: [NSNumber]) -> [CallStackInfo] { 25 | addresses 26 | .lazy 27 | .compactMap { address -> (NSNumber, Dl_info)? in 28 | guard let info = symbolInfo(for: address.uintValue) else { 29 | return nil 30 | } 31 | return (address, info) 32 | } 33 | .map { address, info in 34 | return CallStackInfo( 35 | libraryPath: info.dli_fname.map { String(cString: $0) }, 36 | libraryBaseAddress: info.dli_fbase, 37 | symbolName: info.dli_sname.map { String(cString: $0) }, 38 | symbolAddress: info.dli_saddr, 39 | returnAddress: address 40 | ) 41 | } 42 | } 43 | 44 | /// Retrieves the symbol names for a given set of addresses. 45 | /// 46 | /// - Parameter addresses: An array of addresses for which to retrieve symbol names. 47 | /// - Returns: An array of strings containing the symbol names for each address. 48 | /// 49 | /// Symbol names are returned demangled 50 | package class func callStackNames(addresses: [NSNumber]) -> [String] { 51 | Self.callStackDLInfos(addresses: addresses).compactMap { 52 | if let cname = $0.dli_sname { 53 | let name = String(cString: cname) 54 | return stdlib_demangleName(name) 55 | } 56 | return nil 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/PropertyTracerSupport/Model/CallStackInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallStackInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/10/31. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// A structure representing information about a particular frame in a call stack. 12 | public struct CallStackInfo: Equatable { 13 | 14 | /// The file path of the library in which the symbol is defined, if available. 15 | public let libraryPath: String? 16 | 17 | /// The base address of the library, if available. 18 | public let libraryBaseAddress: UnsafeMutableRawPointer? 19 | 20 | /// The name of the symbol at the call stack frame, if available. 21 | public let symbolName: String? 22 | 23 | /// The address of the symbol, if available. 24 | public let symbolAddress: UnsafeMutableRawPointer? 25 | 26 | /// The return address of the call stack frame. 27 | public let returnAddress: NSNumber 28 | } 29 | 30 | extension CallStackInfo { 31 | 32 | /// The demangled name of the symbol, if available. 33 | /// 34 | /// This computed property attempts to demangle the symbol name, returning `nil` if the symbol name is not available or cannot be demangled. 35 | public var demangledSymbolName: String? { 36 | symbolName.map { 37 | stdlib_demangleName($0) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/PropertyTracerSupport/Model/PropertyAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallerInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/10/31. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// A structure representing access to a property. 12 | public struct PropertyAccess { 13 | 14 | /// A structure representing changes to a property's value. 15 | public struct Changes { 16 | /// The current value of the property before the change. 17 | public let currentValue: V 18 | 19 | /// The new value that the property will change to. 20 | public let newValue: V 21 | 22 | /// Creates a new Changes instance. 23 | /// 24 | /// - Parameters: 25 | /// - currentValue: The current value of the property. 26 | /// - newValue: The new value that the property will change to. 27 | package init( 28 | currentValue: V, 29 | newValue: V 30 | ) { 31 | self.currentValue = currentValue 32 | self.newValue = newValue 33 | } 34 | } 35 | 36 | /// An enumeration representing the type of access (getter or setter). 37 | public enum Accessor { 38 | /// A case representing a getter access. 39 | case getter(V) 40 | 41 | /// A case representing a setter access. 42 | case setter(Changes) 43 | } 44 | 45 | /// The type of access. 46 | public let accessor: Accessor 47 | 48 | /// The call stack information at the point of access. 49 | public let callStackInfo: CallStackInfo 50 | 51 | /// The parent object containing the property, if available. 52 | public private(set) var parent: P? 53 | 54 | /// The key path to the property, if available. 55 | public private(set) var keyPath: KeyPath? 56 | 57 | /// Creates a new PropertyAccess instance. 58 | /// 59 | /// - Parameters: 60 | /// - accessor: The type of access. 61 | /// - callStackInfo: The call stack information at the point of access. 62 | /// - parent: The parent object containing the property. 63 | /// - keyPath: The key path to the property. 64 | package init( 65 | accessor: Accessor, 66 | callStackInfo: CallStackInfo, 67 | parent: P? = nil, 68 | keyPath: KeyPath? = nil 69 | ) { 70 | self.accessor = accessor 71 | self.callStackInfo = callStackInfo 72 | self.parent = parent 73 | self.keyPath = keyPath 74 | } 75 | } 76 | 77 | extension PropertyAccess.Changes: Equatable where V: Equatable {} 78 | 79 | extension PropertyAccess.Accessor: CustomStringConvertible { 80 | public var description: String { 81 | switch self { 82 | case .getter(let v): 83 | return "getter(\(V.self)): \(v)" 84 | case .setter(let changes): 85 | return "setter(\(V.self)): \(changes.currentValue) => \(changes.newValue)" 86 | } 87 | } 88 | } 89 | 90 | /// A type-erased version of PropertyAccess. 91 | public struct AnyPropertyAccess { 92 | 93 | /// A structure representing changes to a property's value in a type-erased manner. 94 | public struct Changes { 95 | /// The current value of the property before the change. 96 | public let currentValue: Any? 97 | 98 | /// The new value that the property will change to. 99 | public let newValue: Any? 100 | } 101 | 102 | /// An enumeration representing the type of access (getter or setter) in a type-erased manner. 103 | public enum Accessor { 104 | /// A case representing a getter access. 105 | case getter(Any?) 106 | 107 | /// A case representing a setter access. 108 | case setter(Changes) 109 | 110 | /// Creates a new Accessor instance from a typed `PropertyAccess.Accessor` instance. 111 | /// 112 | /// - Parameter access: A typed `PropertyAccess.Accessor` instance. 113 | init(_ access: PropertyAccess.Accessor) { 114 | switch access { 115 | case .getter(let v): 116 | self = .getter(v) 117 | case .setter(let changes): 118 | self = .setter( 119 | .init( 120 | currentValue: changes.currentValue, 121 | newValue: changes.newValue 122 | ) 123 | ) 124 | } 125 | } 126 | } 127 | 128 | /// The type of access in a type-erased manner. 129 | public let accessor: Accessor 130 | 131 | /// The call stack information at the point of access. 132 | public let callStackInfo: CallStackInfo 133 | 134 | /// The parent object containing the property, if available, in a type-erased manner. 135 | public private(set) var parent: Any? 136 | 137 | /// The key path to the property, if available, in a type-erased manner. 138 | public private(set) var keyPath: AnyKeyPath? 139 | 140 | /// Creates a new AnyPropertyAccess instance from a typed `PropertyAccess` instance. 141 | /// 142 | /// - Parameter access: A typed `PropertyAccess` instance. 143 | public init(_ access: PropertyAccess) { 144 | self.accessor = Accessor(access.accessor) 145 | self.callStackInfo = access.callStackInfo 146 | self.parent = access.parent 147 | self.keyPath = access.keyPath 148 | } 149 | } 150 | 151 | extension AnyPropertyAccess.Accessor: CustomStringConvertible { 152 | public var description: String { 153 | switch self { 154 | case .getter(let v): 155 | return "getter(\(type(of: v)): \(String(describing: v))" 156 | case .setter(let changes): 157 | return "setter(\(type(of: changes.newValue)): \(String(describing: changes.currentValue)) => \(String(describing: changes.newValue))" 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/PropertyTracerSupport/util/demangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // demangle.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/10/31. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | @_silgen_name("swift_demangle") 12 | func _stdlib_demangleImpl( 13 | mangledName: UnsafePointer?, 14 | mangledNameLength: UInt, 15 | outputBuffer: UnsafeMutablePointer?, 16 | outputBufferSize: UnsafeMutablePointer?, 17 | flags: UInt32 18 | ) -> UnsafeMutablePointer? 19 | 20 | @usableFromInline 21 | func stdlib_demangleName(_ mangledName: String) -> String { 22 | mangledName.utf8CString.withUnsafeBufferPointer { 23 | mangledNameUTF8CStr in 24 | 25 | let demangledNamePtr = _stdlib_demangleImpl( 26 | mangledName: mangledNameUTF8CStr.baseAddress, 27 | mangledNameLength: UInt(mangledNameUTF8CStr.count - 1), 28 | outputBuffer: nil, 29 | outputBufferSize: nil, 30 | flags: 0) 31 | 32 | if let demangledNamePtr = demangledNamePtr { 33 | let demangledName = String(cString: demangledNamePtr) 34 | free(demangledNamePtr) 35 | return demangledName 36 | } 37 | return mangledName 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PropertyTracerSupport/util/symbolInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // symbolInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/10/31. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | func symbolInfo(for address: UInt) -> Dl_info? { 12 | var info = Dl_info() 13 | let ptr: UnsafeRawPointer = UnsafeRawPointer(bitPattern: address)! 14 | let result = dladdr(ptr, &info) 15 | return result == 0 ? nil : info 16 | } 17 | -------------------------------------------------------------------------------- /Tests/PropertyTracerTests/PropertyTracerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PropertyTracer 3 | @testable import PropertyTracerSupport 4 | 5 | final class PropertyTracerTests: XCTestCase { 6 | @PropertyTraced(trace(_:_:)) 7 | class ClassType1 { 8 | static let staticVar = "" 9 | var value1: String = "こんにちは" 10 | var value2: Int = 12 11 | 12 | @NoTraced 13 | var value3: Double = 1.0 14 | 15 | func modify() { 16 | value1 = "hello" 17 | value2 *= 2 18 | value3 = 14 19 | } 20 | } 21 | 22 | @PropertyTraced(trace(_:_:)) 23 | struct StructType1 { 24 | static let staticVar = "" 25 | var value1: String = "こんにちは" 26 | var value2: Int = 12 27 | 28 | @NoTraced 29 | var value3: Double = 1.0 30 | 31 | mutating func modify() { 32 | value1 = "hello" 33 | value2 *= 2 34 | value3 = 14 35 | } 36 | } 37 | 38 | func testClassType1() { 39 | let type = ClassType1() 40 | 41 | type.value1 = "おはよう" 42 | type.value2 = 5 43 | type.value3 = 3333 44 | 45 | type.modify() 46 | } 47 | 48 | func testStructType1() { 49 | var type = StructType1() 50 | 51 | type.value1 = "おはよう" 52 | type.value2 = 5 53 | type.value3 = 3333 54 | 55 | type.modify() 56 | } 57 | } 58 | extension PropertyTracerTests { 59 | class ClassType2 { 60 | @Traced(trace(_:tracedKeyPath:)) 61 | var value1: String = "こんにちは" 62 | 63 | @Traced(trace(_:tracedKeyPath:)) 64 | var value2: Int = 12 65 | 66 | @Traced(trace(_:tracedKeyPath:)) 67 | var value3: Double = 1.0 68 | 69 | init() { 70 | _value2.keyPath.value = \Self.value2 71 | _value3.keyPath.value = \Self.value3 72 | 73 | _value3.parent.value = { [weak self] in self } 74 | 75 | _value3.tracedKeyPath.value = \Self._value3 76 | } 77 | 78 | func modify() { 79 | value1 = "hello" 80 | value2 *= 2 81 | value3 = 14 82 | } 83 | } 84 | 85 | struct StructType2 { 86 | @Traced(trace(_:tracedKeyPath:)) 87 | var value1: String = "こんにちは" 88 | 89 | @Traced(keyPath: \Self.value2, trace(_:tracedKeyPath:)) 90 | var value2: Int = 12 91 | 92 | @Traced(keyPath: \Self..value3, trace(_:tracedKeyPath:)) 93 | var value3: Double = 1.0 94 | 95 | @Traced(keyPath: \Self.value3, trace(_:tracedKeyPath:)) 96 | var value4: Double = 1.0 97 | 98 | init() { 99 | _value3.parent.value = copiedSelf 100 | _value3.tracedKeyPath.value = \Self._value3 101 | } 102 | 103 | mutating func modify() { 104 | value1 = "hello" 105 | value2 *= 2 106 | value3 = 14 107 | } 108 | 109 | func copiedSelf() -> Self { 110 | let copied = self 111 | return copied 112 | } 113 | } 114 | 115 | func testStructType2() { 116 | var type = StructType2() 117 | 118 | type.value1 = "おはよう" 119 | type.value2 = 5 120 | type.value3 = 3333 121 | 122 | type.modify() 123 | } 124 | 125 | func testClassType2() { 126 | let type = ClassType2() 127 | 128 | type.value1 = "おはよう" 129 | type.value2 = 5 130 | type.value3 = 3333 131 | 132 | type.modify() 133 | } 134 | } 135 | 136 | func trace(_ access: AnyPropertyAccess, _ tracedKeyPath: AnyKeyPath?) { 137 | print("\n[Access]------------------") 138 | print("\(access.accessor.description)") 139 | print("called from: \(access.callStackInfo.demangledSymbolName ?? "unknown")") 140 | if let parent = access.parent { 141 | print("parent: \(parent)") 142 | } 143 | if let keyPath = access.keyPath { 144 | print("keyPath: \(keyPath)") 145 | } 146 | if let tracedKeyPath { 147 | print("tracedKeyPath: \(tracedKeyPath)") 148 | } 149 | print("----------------------------") 150 | } 151 | 152 | func trace(_ access: PropertyAccess, tracedKeyPath: KeyPath>?) { 153 | print("\n[Access]------------------") 154 | print("\(access.accessor.description)") 155 | print("called from: \(access.callStackInfo.demangledSymbolName ?? "unknown")") 156 | if let parent = access.parent { 157 | print("parent: \(parent)") 158 | } 159 | if let keyPath = access.keyPath { 160 | print("keyPath: \(keyPath)") 161 | } 162 | if let tracedKeyPath { 163 | print("tracedKeyPath: \(tracedKeyPath)") 164 | } 165 | print("----------------------------") 166 | } 167 | --------------------------------------------------------------------------------