├── .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 | [](https://github.com/p-x9/swift-property-tracer/issues)
8 | [](https://github.com/p-x9/swift-property-tracer/network/members)
9 | [](https://github.com/p-x9/swift-property-tracer/stargazers)
10 | [](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 |
--------------------------------------------------------------------------------