memorydetective 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +76 -34
- package/USAGE.md +112 -41
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/axe.d.ts +86 -0
- package/dist/runtime/axe.js +249 -0
- package/dist/runtime/axe.js.map +1 -0
- package/dist/runtime/buildSettings.d.ts +27 -0
- package/dist/runtime/buildSettings.js +88 -0
- package/dist/runtime/buildSettings.js.map +1 -0
- package/dist/runtime/exec.d.ts +8 -1
- package/dist/runtime/exec.js +8 -2
- package/dist/runtime/exec.js.map +1 -1
- package/dist/runtime/fixTemplates.d.ts +27 -0
- package/dist/runtime/fixTemplates.js +757 -0
- package/dist/runtime/fixTemplates.js.map +1 -0
- package/dist/runtime/simctl.d.ts +68 -0
- package/dist/runtime/simctl.js +194 -0
- package/dist/runtime/simctl.js.map +1 -0
- package/dist/runtime/staticAnalysisHints.js +8 -0
- package/dist/runtime/staticAnalysisHints.js.map +1 -1
- package/dist/tools/bootAndLaunchForLeakInvestigation.d.ts +166 -0
- package/dist/tools/bootAndLaunchForLeakInvestigation.js +367 -0
- package/dist/tools/bootAndLaunchForLeakInvestigation.js.map +1 -0
- package/dist/tools/captureMemgraph.d.ts +29 -1
- package/dist/tools/captureMemgraph.js +148 -6
- package/dist/tools/captureMemgraph.js.map +1 -1
- package/dist/tools/captureScenarioState.d.ts +77 -0
- package/dist/tools/captureScenarioState.js +159 -0
- package/dist/tools/captureScenarioState.js.map +1 -0
- package/dist/tools/classifyCycle.d.ts +7 -0
- package/dist/tools/classifyCycle.js +31 -0
- package/dist/tools/classifyCycle.js.map +1 -1
- package/dist/tools/compareTracesByPattern.d.ts +112 -0
- package/dist/tools/compareTracesByPattern.js +312 -0
- package/dist/tools/compareTracesByPattern.js.map +1 -0
- package/dist/tools/detectLeaksInXCUITest.d.ts +2 -2
- package/dist/tools/getInvestigationPlaybook.d.ts +15 -0
- package/dist/tools/getInvestigationPlaybook.js +24 -1
- package/dist/tools/getInvestigationPlaybook.js.map +1 -1
- package/dist/tools/replayScenario.d.ts +243 -0
- package/dist/tools/replayScenario.js +187 -0
- package/dist/tools/replayScenario.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-pattern fix templates: Swift code snippets showing the typical
|
|
3
|
+
* before/after for each cycle pattern in the catalog. Pairs with
|
|
4
|
+
* `staticAnalysisHints.ts` and `classifyCycle.PATTERNS` to give the
|
|
5
|
+
* agent a concrete code example it can adapt to the user's context.
|
|
6
|
+
*
|
|
7
|
+
* Templates are deliberately minimal — just enough to demonstrate the
|
|
8
|
+
* shape of the fix. The agent fills in real type/method names from the
|
|
9
|
+
* surrounding code via the SourceKit-LSP tools.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Pattern-id → fix template. Every pattern in `classifyCycle.PATTERNS`
|
|
13
|
+
* has an entry. Coverage is enforced by a 1:1 test guard.
|
|
14
|
+
*/
|
|
15
|
+
const TEMPLATES = {
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// v1.0 core
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
19
|
+
"swiftui.tag-index-projection": {
|
|
20
|
+
context: "SwiftUI ForEach + .tag() with self-capturing closure",
|
|
21
|
+
before: `ForEach(items) { item in
|
|
22
|
+
Cell(item: item)
|
|
23
|
+
.onTapGesture {
|
|
24
|
+
self.viewModel.handleTap(item)
|
|
25
|
+
}
|
|
26
|
+
.tag(item.id)
|
|
27
|
+
}`,
|
|
28
|
+
after: `ForEach(items) { item in
|
|
29
|
+
Cell(item: item)
|
|
30
|
+
.onTapGesture { [weak vm = self.viewModel] in
|
|
31
|
+
vm?.handleTap(item)
|
|
32
|
+
}
|
|
33
|
+
.tag(item.id)
|
|
34
|
+
}`,
|
|
35
|
+
notes: "If the closure captures multiple things from self, weak-capture each one explicitly. Avoid `[weak self]` because TagIndexProjection often needs the references resolved synchronously.",
|
|
36
|
+
},
|
|
37
|
+
"swiftui.dictstorage-weakbox-cycle": {
|
|
38
|
+
context: "SwiftUI internal observation cycle. Find your app-level types in the chain and break the strong capture there.",
|
|
39
|
+
before: `// Look up the chain in the memgraph for your app-level types.
|
|
40
|
+
// The cycle root is _DictionaryStorage<...WeakBox<AnyLocationBase>>;
|
|
41
|
+
// the user-fixable side is whatever closure captures self below it.
|
|
42
|
+
class MyViewModel: ObservableObject {
|
|
43
|
+
@Published var items: [Item] = []
|
|
44
|
+
func bind() {
|
|
45
|
+
SomePublisher.assign(to: \\.items, on: self) // ⚠️ retains self
|
|
46
|
+
}
|
|
47
|
+
}`,
|
|
48
|
+
after: `class MyViewModel: ObservableObject {
|
|
49
|
+
@Published var items: [Item] = []
|
|
50
|
+
func bind() {
|
|
51
|
+
SomePublisher.assign(to: &$items) // OK — auto-cancels with @Published
|
|
52
|
+
}
|
|
53
|
+
}`,
|
|
54
|
+
},
|
|
55
|
+
"swiftui.foreach-state-tap": {
|
|
56
|
+
before: `ForEach(items) { item in
|
|
57
|
+
Cell(item: item)
|
|
58
|
+
.onTapGesture {
|
|
59
|
+
self.handleTap(item) // captures self strongly
|
|
60
|
+
}
|
|
61
|
+
}`,
|
|
62
|
+
after: `ForEach(items) { item in
|
|
63
|
+
Cell(item: item)
|
|
64
|
+
.onTapGesture { [weak self] in
|
|
65
|
+
self?.handleTap(item)
|
|
66
|
+
}
|
|
67
|
+
}`,
|
|
68
|
+
notes: "Or make handleTap a static helper that takes the dependencies as parameters.",
|
|
69
|
+
},
|
|
70
|
+
"closure.viewmodel-wrapped-strong": {
|
|
71
|
+
context: "Closure captures `_viewModel.wrappedValue` strongly via the property wrapper.",
|
|
72
|
+
before: `struct MyView: View {
|
|
73
|
+
@StateObject var viewModel: MyViewModel
|
|
74
|
+
var body: some View {
|
|
75
|
+
Button("Tap") {
|
|
76
|
+
self._viewModel.wrappedValue.handleTap() // strong capture
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}`,
|
|
80
|
+
after: `struct MyView: View {
|
|
81
|
+
@StateObject var viewModel: MyViewModel
|
|
82
|
+
var body: some View {
|
|
83
|
+
Button("Tap") { [weak vm = _viewModel.wrappedValue] in
|
|
84
|
+
vm?.handleTap()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}`,
|
|
88
|
+
},
|
|
89
|
+
"viewcontroller.uinavigationcontroller-host": {
|
|
90
|
+
context: "UIViewControllerRepresentable wrapping a UINavigationController. Clear the stack on dismantle.",
|
|
91
|
+
before: `struct NavWrapper: UIViewControllerRepresentable {
|
|
92
|
+
func makeUIViewController(context: Context) -> UINavigationController {
|
|
93
|
+
UINavigationController(rootViewController: UIHostingController(rootView: ChildView()))
|
|
94
|
+
}
|
|
95
|
+
func updateUIViewController(_: UINavigationController, context: Context) {}
|
|
96
|
+
// ⚠️ no dismantleUIViewController — the host->VC->host cycle stays alive
|
|
97
|
+
}`,
|
|
98
|
+
after: `struct NavWrapper: UIViewControllerRepresentable {
|
|
99
|
+
func makeUIViewController(context: Context) -> UINavigationController {
|
|
100
|
+
UINavigationController(rootViewController: UIHostingController(rootView: ChildView()))
|
|
101
|
+
}
|
|
102
|
+
func updateUIViewController(_: UINavigationController, context: Context) {}
|
|
103
|
+
static func dismantleUIViewController(_ uiVC: UINavigationController, coordinator: ()) {
|
|
104
|
+
uiVC.viewControllers = [] // breaks the cycle
|
|
105
|
+
}
|
|
106
|
+
}`,
|
|
107
|
+
},
|
|
108
|
+
"combine.sink-store-self-capture": {
|
|
109
|
+
before: `class VM: ObservableObject {
|
|
110
|
+
@Published var value = 0
|
|
111
|
+
private var bag = Set<AnyCancellable>()
|
|
112
|
+
func observe(_ pub: AnyPublisher<Int, Never>) {
|
|
113
|
+
pub.sink { v in self.value = v }.store(in: &bag) // ⚠️ retains self
|
|
114
|
+
}
|
|
115
|
+
}`,
|
|
116
|
+
after: `class VM: ObservableObject {
|
|
117
|
+
@Published var value = 0
|
|
118
|
+
private var bag = Set<AnyCancellable>()
|
|
119
|
+
func observe(_ pub: AnyPublisher<Int, Never>) {
|
|
120
|
+
pub.sink { [weak self] v in self?.value = v }.store(in: &bag)
|
|
121
|
+
// OR for property-path: pub.assign(to: &$value)
|
|
122
|
+
}
|
|
123
|
+
}`,
|
|
124
|
+
},
|
|
125
|
+
"concurrency.task-without-weak-self": {
|
|
126
|
+
before: `class VM {
|
|
127
|
+
func startWatching() {
|
|
128
|
+
Task {
|
|
129
|
+
for await event in stream {
|
|
130
|
+
self.handle(event) // strong capture for task lifetime
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}`,
|
|
135
|
+
after: `class VM {
|
|
136
|
+
private var task: Task<Void, Never>?
|
|
137
|
+
func startWatching() {
|
|
138
|
+
task = Task { [weak self] in
|
|
139
|
+
for await event in stream {
|
|
140
|
+
guard let self else { break }
|
|
141
|
+
self.handle(event)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
deinit { task?.cancel() }
|
|
146
|
+
}`,
|
|
147
|
+
},
|
|
148
|
+
"notificationcenter.observer-strong": {
|
|
149
|
+
before: `class VC: UIViewController {
|
|
150
|
+
var token: NSObjectProtocol?
|
|
151
|
+
override func viewDidLoad() {
|
|
152
|
+
token = NotificationCenter.default.addObserver(
|
|
153
|
+
forName: .someNotif, object: nil, queue: .main
|
|
154
|
+
) { _ in
|
|
155
|
+
self.refresh() // strong capture
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}`,
|
|
159
|
+
after: `class VC: UIViewController {
|
|
160
|
+
var token: NSObjectProtocol?
|
|
161
|
+
override func viewDidLoad() {
|
|
162
|
+
token = NotificationCenter.default.addObserver(
|
|
163
|
+
forName: .someNotif, object: nil, queue: .main
|
|
164
|
+
) { [weak self] _ in
|
|
165
|
+
self?.refresh()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
deinit {
|
|
169
|
+
if let token { NotificationCenter.default.removeObserver(token) }
|
|
170
|
+
}
|
|
171
|
+
}`,
|
|
172
|
+
},
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
174
|
+
// v1.4 expansion
|
|
175
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
176
|
+
"timer.scheduled-target-strong": {
|
|
177
|
+
before: `class VC {
|
|
178
|
+
var timer: Timer?
|
|
179
|
+
func start() {
|
|
180
|
+
timer = Timer.scheduledTimer(timeInterval: 1, target: self,
|
|
181
|
+
selector: #selector(tick), userInfo: nil, repeats: true)
|
|
182
|
+
}
|
|
183
|
+
@objc func tick() { /* ... */ }
|
|
184
|
+
}`,
|
|
185
|
+
after: `class VC {
|
|
186
|
+
var timer: Timer?
|
|
187
|
+
func start() {
|
|
188
|
+
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
|
189
|
+
self?.tick()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
@objc func tick() { /* ... */ }
|
|
193
|
+
deinit { timer?.invalidate() }
|
|
194
|
+
}`,
|
|
195
|
+
},
|
|
196
|
+
"displaylink.target-strong": {
|
|
197
|
+
before: `class Renderer {
|
|
198
|
+
var link: CADisplayLink?
|
|
199
|
+
func start() {
|
|
200
|
+
link = CADisplayLink(target: self, selector: #selector(step))
|
|
201
|
+
link?.add(to: .main, forMode: .common)
|
|
202
|
+
}
|
|
203
|
+
@objc func step(_ link: CADisplayLink) { /* ... */ }
|
|
204
|
+
}`,
|
|
205
|
+
after: `final class WeakProxy<T: AnyObject>: NSObject {
|
|
206
|
+
weak var target: T?
|
|
207
|
+
init(_ target: T) { self.target = target }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class Renderer {
|
|
211
|
+
var link: CADisplayLink?
|
|
212
|
+
private var proxy: WeakProxy<Renderer>?
|
|
213
|
+
func start() {
|
|
214
|
+
proxy = WeakProxy(self)
|
|
215
|
+
link = CADisplayLink(target: proxy!, selector: #selector(WeakProxy<Renderer>.forward(_:)))
|
|
216
|
+
link?.add(to: .main, forMode: .common)
|
|
217
|
+
}
|
|
218
|
+
@objc func step(_ link: CADisplayLink) { /* ... */ }
|
|
219
|
+
deinit { link?.invalidate() }
|
|
220
|
+
}`,
|
|
221
|
+
notes: "WeakProxy is a one-time helper you can put in a Utilities folder.",
|
|
222
|
+
},
|
|
223
|
+
"gesture.target-strong": {
|
|
224
|
+
before: `class VC: UIViewController {
|
|
225
|
+
override func viewDidLoad() {
|
|
226
|
+
let tap = UITapGestureRecognizer(target: self, action: #selector(onTap))
|
|
227
|
+
view.addGestureRecognizer(tap)
|
|
228
|
+
}
|
|
229
|
+
@objc func onTap() { /* ... */ }
|
|
230
|
+
}`,
|
|
231
|
+
after: `class VC: UIViewController {
|
|
232
|
+
override func viewDidLoad() {
|
|
233
|
+
// iOS 14+: closure-style action, UIKit handles weakly
|
|
234
|
+
let tap = UITapGestureRecognizer(target: nil, action: nil)
|
|
235
|
+
tap.addAction(UIAction { [weak self] _ in self?.onTap() })
|
|
236
|
+
view.addGestureRecognizer(tap)
|
|
237
|
+
}
|
|
238
|
+
func onTap() { /* ... */ }
|
|
239
|
+
}`,
|
|
240
|
+
notes: "Or for selector-form: `tap.removeTarget(self, action: nil)` in `deinit`.",
|
|
241
|
+
},
|
|
242
|
+
"kvo.observation-not-invalidated": {
|
|
243
|
+
before: `class VM {
|
|
244
|
+
var token: NSKeyValueObservation?
|
|
245
|
+
func bind(to obj: SomeKVOClass) {
|
|
246
|
+
token = obj.observe(\\.value) { _, _ in
|
|
247
|
+
self.refresh() // strong capture
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}`,
|
|
251
|
+
after: `class VM {
|
|
252
|
+
var token: NSKeyValueObservation?
|
|
253
|
+
func bind(to obj: SomeKVOClass) {
|
|
254
|
+
token = obj.observe(\\.value) { [weak self] _, _ in
|
|
255
|
+
self?.refresh()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
deinit { token?.invalidate() }
|
|
259
|
+
}`,
|
|
260
|
+
},
|
|
261
|
+
"urlsession.delegate-strong": {
|
|
262
|
+
before: `class APIClient: NSObject, URLSessionDelegate {
|
|
263
|
+
let session: URLSession
|
|
264
|
+
override init() {
|
|
265
|
+
super.init()
|
|
266
|
+
// ⚠️ session retains self as delegate; APIClient never deallocs
|
|
267
|
+
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
|
268
|
+
}
|
|
269
|
+
}`,
|
|
270
|
+
after: `class APIClient: NSObject, URLSessionDelegate {
|
|
271
|
+
let session: URLSession
|
|
272
|
+
override init() {
|
|
273
|
+
super.init()
|
|
274
|
+
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
|
275
|
+
}
|
|
276
|
+
deinit {
|
|
277
|
+
session.invalidateAndCancel() // breaks the strong-delegate retain
|
|
278
|
+
}
|
|
279
|
+
}`,
|
|
280
|
+
},
|
|
281
|
+
"dispatch.source-event-handler-self": {
|
|
282
|
+
before: `class FileWatcher {
|
|
283
|
+
let source: DispatchSourceFileSystemObject
|
|
284
|
+
init(fd: Int32) {
|
|
285
|
+
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: .write)
|
|
286
|
+
source.setEventHandler {
|
|
287
|
+
self.handle() // strong capture
|
|
288
|
+
}
|
|
289
|
+
source.resume()
|
|
290
|
+
}
|
|
291
|
+
}`,
|
|
292
|
+
after: `class FileWatcher {
|
|
293
|
+
let source: DispatchSourceFileSystemObject
|
|
294
|
+
init(fd: Int32) {
|
|
295
|
+
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: .write)
|
|
296
|
+
source.setEventHandler { [weak self] in
|
|
297
|
+
self?.handle()
|
|
298
|
+
}
|
|
299
|
+
source.resume()
|
|
300
|
+
}
|
|
301
|
+
deinit {
|
|
302
|
+
source.setEventHandler {} // clear the closure
|
|
303
|
+
source.cancel()
|
|
304
|
+
}
|
|
305
|
+
}`,
|
|
306
|
+
},
|
|
307
|
+
"notificationcenter.observer-not-removed": {
|
|
308
|
+
before: `class VC: UIViewController {
|
|
309
|
+
var token: NSObjectProtocol?
|
|
310
|
+
override func viewDidLoad() {
|
|
311
|
+
token = NotificationCenter.default.addObserver(
|
|
312
|
+
forName: .someNotif, object: nil, queue: .main
|
|
313
|
+
) { [weak self] _ in self?.refresh() }
|
|
314
|
+
// ⚠️ no removeObserver in deinit — observer stays in NotificationCenter forever
|
|
315
|
+
}
|
|
316
|
+
}`,
|
|
317
|
+
after: `class VC: UIViewController {
|
|
318
|
+
var token: NSObjectProtocol?
|
|
319
|
+
override func viewDidLoad() {
|
|
320
|
+
token = NotificationCenter.default.addObserver(
|
|
321
|
+
forName: .someNotif, object: nil, queue: .main
|
|
322
|
+
) { [weak self] _ in self?.refresh() }
|
|
323
|
+
}
|
|
324
|
+
deinit {
|
|
325
|
+
if let token { NotificationCenter.default.removeObserver(token) }
|
|
326
|
+
}
|
|
327
|
+
}`,
|
|
328
|
+
},
|
|
329
|
+
"delegate.strong-reference": {
|
|
330
|
+
before: `protocol MyServiceDelegate: AnyObject { /* ... */ }
|
|
331
|
+
class MyService {
|
|
332
|
+
var delegate: MyServiceDelegate? // no \`weak\` modifier
|
|
333
|
+
}`,
|
|
334
|
+
after: `protocol MyServiceDelegate: AnyObject { /* ... */ }
|
|
335
|
+
class MyService {
|
|
336
|
+
weak var delegate: MyServiceDelegate?
|
|
337
|
+
}`,
|
|
338
|
+
},
|
|
339
|
+
"swiftui.envobject-back-reference": {
|
|
340
|
+
before: `class AppViewModel: ObservableObject {
|
|
341
|
+
var hostingController: UIHostingController<RootView>? // ⚠️ strong UIKit ref
|
|
342
|
+
}`,
|
|
343
|
+
after: `class AppViewModel: ObservableObject {
|
|
344
|
+
weak var hostingController: UIHostingController<RootView>?
|
|
345
|
+
}`,
|
|
346
|
+
notes: "If the bridge has to be strong, refactor: pass the controller into the few methods that need it instead of storing it.",
|
|
347
|
+
},
|
|
348
|
+
"combine.assign-to-self": {
|
|
349
|
+
before: `class VM: ObservableObject {
|
|
350
|
+
@Published var x = 0
|
|
351
|
+
var bag = Set<AnyCancellable>()
|
|
352
|
+
func observe(_ pub: AnyPublisher<Int, Never>) {
|
|
353
|
+
pub.assign(to: \\.x, on: self).store(in: &bag) // ⚠️ retains self
|
|
354
|
+
}
|
|
355
|
+
}`,
|
|
356
|
+
after: `class VM: ObservableObject {
|
|
357
|
+
@Published var x = 0
|
|
358
|
+
func observe(_ pub: AnyPublisher<Int, Never>) {
|
|
359
|
+
pub.assign(to: &$x) // auto-cancels with @Published
|
|
360
|
+
}
|
|
361
|
+
}`,
|
|
362
|
+
},
|
|
363
|
+
"concurrency.task-mainactor-view": {
|
|
364
|
+
before: `struct MyView: View {
|
|
365
|
+
@StateObject var viewModel: VM
|
|
366
|
+
var body: some View {
|
|
367
|
+
Text(viewModel.label)
|
|
368
|
+
.onAppear {
|
|
369
|
+
Task { await self.viewModel.refresh() } // pins view storage
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}`,
|
|
373
|
+
after: `struct MyView: View {
|
|
374
|
+
@StateObject var viewModel: VM
|
|
375
|
+
var body: some View {
|
|
376
|
+
Text(viewModel.label)
|
|
377
|
+
.task { // auto-cancelled when the view leaves
|
|
378
|
+
await viewModel.refresh()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}`,
|
|
382
|
+
},
|
|
383
|
+
"concurrency.asyncstream-continuation-self": {
|
|
384
|
+
before: `class Producer {
|
|
385
|
+
private var task: Task<Void, Never>?
|
|
386
|
+
let stream: AsyncStream<Event>
|
|
387
|
+
init() {
|
|
388
|
+
stream = AsyncStream { continuation in
|
|
389
|
+
// ⚠️ continuation captures producer; producer captures stream
|
|
390
|
+
self.subscribe { event in continuation.yield(event) }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}`,
|
|
394
|
+
after: `class Producer {
|
|
395
|
+
private var task: Task<Void, Never>?
|
|
396
|
+
let stream: AsyncStream<Event>
|
|
397
|
+
init() {
|
|
398
|
+
stream = AsyncStream { [weak self] continuation in
|
|
399
|
+
self?.subscribe { event in continuation.yield(event) }
|
|
400
|
+
continuation.onTermination = { [weak self] _ in self?.unsubscribe() }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
deinit { task?.cancel() }
|
|
404
|
+
}`,
|
|
405
|
+
},
|
|
406
|
+
"webkit.scriptmessage-handler-strong": {
|
|
407
|
+
before: `class WebVC: UIViewController, WKScriptMessageHandler {
|
|
408
|
+
var webView: WKWebView!
|
|
409
|
+
override func viewDidLoad() {
|
|
410
|
+
let cfg = WKWebViewConfiguration()
|
|
411
|
+
cfg.userContentController.add(self, name: "bridge") // ⚠️ retains self
|
|
412
|
+
webView = WKWebView(frame: view.bounds, configuration: cfg)
|
|
413
|
+
view.addSubview(webView)
|
|
414
|
+
}
|
|
415
|
+
func userContentController(_ ucc: WKUserContentController, didReceive m: WKScriptMessage) { /* */ }
|
|
416
|
+
}`,
|
|
417
|
+
after: `class WebVC: UIViewController, WKScriptMessageHandler {
|
|
418
|
+
var webView: WKWebView!
|
|
419
|
+
override func viewDidLoad() {
|
|
420
|
+
let cfg = WKWebViewConfiguration()
|
|
421
|
+
cfg.userContentController.add(self, name: "bridge")
|
|
422
|
+
webView = WKWebView(frame: view.bounds, configuration: cfg)
|
|
423
|
+
view.addSubview(webView)
|
|
424
|
+
}
|
|
425
|
+
deinit {
|
|
426
|
+
webView.configuration.userContentController.removeScriptMessageHandler(forName: "bridge")
|
|
427
|
+
}
|
|
428
|
+
func userContentController(_ ucc: WKUserContentController, didReceive m: WKScriptMessage) { /* */ }
|
|
429
|
+
}`,
|
|
430
|
+
notes: "Or use a WeakScriptMessageHandler proxy class — see the v1.6 webkit.wkscriptmessagehandler-bridge fix template.",
|
|
431
|
+
},
|
|
432
|
+
"coordinator.parent-strong-back-reference": {
|
|
433
|
+
before: `class ChildCoordinator {
|
|
434
|
+
var parentCoordinator: AppCoordinator? // ⚠️ no \`weak\`
|
|
435
|
+
}
|
|
436
|
+
class AppCoordinator {
|
|
437
|
+
var childCoordinators: [ChildCoordinator] = []
|
|
438
|
+
}`,
|
|
439
|
+
after: `class ChildCoordinator {
|
|
440
|
+
weak var parentCoordinator: AppCoordinator?
|
|
441
|
+
}
|
|
442
|
+
class AppCoordinator {
|
|
443
|
+
var childCoordinators: [ChildCoordinator] = []
|
|
444
|
+
func childDidFinish(_ child: ChildCoordinator) {
|
|
445
|
+
childCoordinators.removeAll { $0 === child }
|
|
446
|
+
}
|
|
447
|
+
}`,
|
|
448
|
+
},
|
|
449
|
+
"rxswift.disposebag-self-cycle": {
|
|
450
|
+
before: `class VM {
|
|
451
|
+
let bag = DisposeBag()
|
|
452
|
+
func observe(_ obs: Observable<Int>) {
|
|
453
|
+
obs.subscribe(onNext: self.handle).disposed(by: bag) // ⚠️ unbound method ref
|
|
454
|
+
}
|
|
455
|
+
func handle(_ value: Int) { /* */ }
|
|
456
|
+
}`,
|
|
457
|
+
after: `class VM {
|
|
458
|
+
let bag = DisposeBag()
|
|
459
|
+
func observe(_ obs: Observable<Int>) {
|
|
460
|
+
obs.subscribe(onNext: { [weak self] v in self?.handle(v) }).disposed(by: bag)
|
|
461
|
+
}
|
|
462
|
+
func handle(_ value: Int) { /* */ }
|
|
463
|
+
}`,
|
|
464
|
+
},
|
|
465
|
+
"realm.notificationtoken-retained": {
|
|
466
|
+
before: `class VM {
|
|
467
|
+
var token: NotificationToken?
|
|
468
|
+
func observe(_ results: Results<Item>) {
|
|
469
|
+
token = results.observe { _ in
|
|
470
|
+
self.refresh() // strong capture
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}`,
|
|
474
|
+
after: `class VM {
|
|
475
|
+
var token: NotificationToken?
|
|
476
|
+
func observe(_ results: Results<Item>) {
|
|
477
|
+
token = results.observe { [weak self] _ in
|
|
478
|
+
self?.refresh()
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
deinit { token?.invalidate() }
|
|
482
|
+
}`,
|
|
483
|
+
},
|
|
484
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
485
|
+
// v1.5 catalog completion
|
|
486
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
487
|
+
"coreanimation.animation-delegate-strong": {
|
|
488
|
+
before: `class FadeView: UIView, CAAnimationDelegate {
|
|
489
|
+
func fadeOut() {
|
|
490
|
+
let anim = CABasicAnimation(keyPath: "opacity")
|
|
491
|
+
anim.delegate = self // ⚠️ CAAnimation.delegate is STRONG (Apple-documented)
|
|
492
|
+
anim.toValue = 0
|
|
493
|
+
layer.add(anim, forKey: "fade")
|
|
494
|
+
}
|
|
495
|
+
func animationDidStop(_ anim: CAAnimation, finished: Bool) { /* */ }
|
|
496
|
+
}`,
|
|
497
|
+
after: `class FadeView: UIView, CAAnimationDelegate {
|
|
498
|
+
func fadeOut() {
|
|
499
|
+
let anim = CABasicAnimation(keyPath: "opacity")
|
|
500
|
+
anim.delegate = self
|
|
501
|
+
anim.toValue = 0
|
|
502
|
+
layer.add(anim, forKey: "fade")
|
|
503
|
+
}
|
|
504
|
+
func animationDidStop(_ anim: CAAnimation, finished: Bool) {
|
|
505
|
+
anim.delegate = nil // breaks the strong cycle when animation ends
|
|
506
|
+
}
|
|
507
|
+
}`,
|
|
508
|
+
notes: "Or wrap in a value-type AnimationProxyDelegate that holds the real owner weakly.",
|
|
509
|
+
},
|
|
510
|
+
"coreanimation.layer-delegate-cycle": {
|
|
511
|
+
before: `class CustomRenderer { // NOT a UIView
|
|
512
|
+
let shapeLayer = CAShapeLayer()
|
|
513
|
+
init() {
|
|
514
|
+
shapeLayer.delegate = self // ⚠️ CALayer.delegate retain pattern
|
|
515
|
+
}
|
|
516
|
+
}`,
|
|
517
|
+
after: `final class WeakLayerDelegate: NSObject, CALayerDelegate {
|
|
518
|
+
weak var owner: CustomRenderer?
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
class CustomRenderer {
|
|
522
|
+
let shapeLayer = CAShapeLayer()
|
|
523
|
+
private let proxy = WeakLayerDelegate()
|
|
524
|
+
init() {
|
|
525
|
+
proxy.owner = self
|
|
526
|
+
shapeLayer.delegate = proxy
|
|
527
|
+
}
|
|
528
|
+
deinit { shapeLayer.delegate = nil }
|
|
529
|
+
}`,
|
|
530
|
+
},
|
|
531
|
+
"coredata.fetchedresultscontroller-delegate": {
|
|
532
|
+
before: `class ListVC: UIViewController, NSFetchedResultsControllerDelegate {
|
|
533
|
+
let frc: NSFetchedResultsController<Item>
|
|
534
|
+
init(...) {
|
|
535
|
+
frc = NSFetchedResultsController(...)
|
|
536
|
+
super.init(...)
|
|
537
|
+
frc.delegate = self // ⚠️ change-tracker retains self
|
|
538
|
+
}
|
|
539
|
+
}`,
|
|
540
|
+
after: `class ListVC: UIViewController, NSFetchedResultsControllerDelegate {
|
|
541
|
+
let frc: NSFetchedResultsController<Item>
|
|
542
|
+
init(...) {
|
|
543
|
+
frc = NSFetchedResultsController(...)
|
|
544
|
+
super.init(...)
|
|
545
|
+
frc.delegate = self
|
|
546
|
+
}
|
|
547
|
+
deinit {
|
|
548
|
+
frc.delegate = nil // explicitly clear before VC dies
|
|
549
|
+
}
|
|
550
|
+
}`,
|
|
551
|
+
},
|
|
552
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
553
|
+
// v1.6 catalog
|
|
554
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
555
|
+
"swiftui.observable-state-modal-leak": {
|
|
556
|
+
before: `@Observable class ItemModel {
|
|
557
|
+
var name = ""
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
struct ParentView: View {
|
|
561
|
+
@State private var showSheet = false
|
|
562
|
+
var body: some View {
|
|
563
|
+
Button("Open") { showSheet = true }
|
|
564
|
+
.sheet(isPresented: $showSheet) {
|
|
565
|
+
// ⚠️ creates a new @State per sheet presentation; older instances leak
|
|
566
|
+
ChildView(model: ItemModel())
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
struct ChildView: View {
|
|
572
|
+
@State var model: ItemModel
|
|
573
|
+
var body: some View { TextField("Name", text: $model.name) }
|
|
574
|
+
}`,
|
|
575
|
+
after: `@Observable class ItemModel {
|
|
576
|
+
var name = ""
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
struct ParentView: View {
|
|
580
|
+
@State private var model = ItemModel() // owned by parent, lifetime stable
|
|
581
|
+
@State private var showSheet = false
|
|
582
|
+
var body: some View {
|
|
583
|
+
Button("Open") { showSheet = true }
|
|
584
|
+
.sheet(isPresented: $showSheet) {
|
|
585
|
+
ChildView(model: model) // pass, don't allocate
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
struct ChildView: View {
|
|
591
|
+
@Bindable var model: ItemModel // @Bindable, not @State
|
|
592
|
+
var body: some View { TextField("Name", text: $model.name) }
|
|
593
|
+
}`,
|
|
594
|
+
},
|
|
595
|
+
"swiftui.navigationpath-stored-in-viewmodel": {
|
|
596
|
+
before: `@Observable class Router {
|
|
597
|
+
var path = NavigationPath() // ⚠️ retains every element ever pushed
|
|
598
|
+
func push<V: Hashable>(_ destination: V) { path.append(destination) }
|
|
599
|
+
}`,
|
|
600
|
+
after: `// Option 1: keep path local to the view
|
|
601
|
+
struct ContentView: View {
|
|
602
|
+
@State private var path = NavigationPath()
|
|
603
|
+
var body: some View {
|
|
604
|
+
NavigationStack(path: $path) { /* ... */ }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Option 2: when path MUST persist on a router, reset after popToRoot
|
|
609
|
+
@Observable class Router {
|
|
610
|
+
var path = NavigationPath()
|
|
611
|
+
func popToRoot() {
|
|
612
|
+
path = NavigationPath() // discard accumulated retention
|
|
613
|
+
}
|
|
614
|
+
}`,
|
|
615
|
+
},
|
|
616
|
+
"concurrency.async-sequence-on-self": {
|
|
617
|
+
before: `class Watcher {
|
|
618
|
+
private var task: Task<Void, Never>?
|
|
619
|
+
func start(_ stream: AsyncStream<Event>) {
|
|
620
|
+
task = Task {
|
|
621
|
+
for await event in stream {
|
|
622
|
+
self.handle(event) // ⚠️ iteration holds self via actor isolation
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}`,
|
|
627
|
+
after: `class Watcher {
|
|
628
|
+
private var task: Task<Void, Never>?
|
|
629
|
+
func start(_ stream: AsyncStream<Event>) {
|
|
630
|
+
task = Task { [weak self] in
|
|
631
|
+
for await event in stream {
|
|
632
|
+
guard let me = self else { break }
|
|
633
|
+
me.handle(event)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
deinit { task?.cancel() }
|
|
638
|
+
}`,
|
|
639
|
+
notes: "The `[weak self]` on the Task is necessary but NOT sufficient on infinite streams — you also need explicit `task.cancel()` in deinit.",
|
|
640
|
+
},
|
|
641
|
+
"concurrency.notificationcenter-async-observer-task": {
|
|
642
|
+
before: `class Listener {
|
|
643
|
+
private var task: Task<Void, Never>?
|
|
644
|
+
init() {
|
|
645
|
+
task = Task {
|
|
646
|
+
// ⚠️ never terminates → never cancels → pins self forever
|
|
647
|
+
for await note in NotificationCenter.default.notifications(named: .myNotif) {
|
|
648
|
+
self.handle(note)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}`,
|
|
653
|
+
after: `class Listener {
|
|
654
|
+
private var task: Task<Void, Never>?
|
|
655
|
+
init() {
|
|
656
|
+
task = Task { [weak self] in
|
|
657
|
+
for await note in NotificationCenter.default.notifications(named: .myNotif) {
|
|
658
|
+
guard let self else { break }
|
|
659
|
+
self.handle(note)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
deinit { task?.cancel() }
|
|
664
|
+
}`,
|
|
665
|
+
},
|
|
666
|
+
"swiftui.observations-closure-strong-self": {
|
|
667
|
+
before: `class WatcherVM {
|
|
668
|
+
let model = MyObservableModel()
|
|
669
|
+
init() {
|
|
670
|
+
Observations { [model] in
|
|
671
|
+
self.refresh(value: model.value) // ⚠️ closure retains self
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
func refresh(value: Int) { /* */ }
|
|
675
|
+
}`,
|
|
676
|
+
after: `class WatcherVM {
|
|
677
|
+
let model = MyObservableModel()
|
|
678
|
+
init() {
|
|
679
|
+
Observations { [model, weak self] in
|
|
680
|
+
self?.refresh(value: model.value)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
func refresh(value: Int) { /* */ }
|
|
684
|
+
}`,
|
|
685
|
+
},
|
|
686
|
+
"webkit.wkscriptmessagehandler-bridge": {
|
|
687
|
+
before: `class WebBridge: NSObject, WKScriptMessageHandler {
|
|
688
|
+
var webView: WKWebView!
|
|
689
|
+
override init() {
|
|
690
|
+
super.init()
|
|
691
|
+
let cfg = WKWebViewConfiguration()
|
|
692
|
+
cfg.userContentController.add(self, name: "native") // ⚠️ 3-link cycle
|
|
693
|
+
webView = WKWebView(frame: .zero, configuration: cfg)
|
|
694
|
+
}
|
|
695
|
+
func userContentController(_: WKUserContentController, didReceive: WKScriptMessage) { /* */ }
|
|
696
|
+
}`,
|
|
697
|
+
after: `final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
|
698
|
+
weak var realHandler: WKScriptMessageHandler?
|
|
699
|
+
init(_ handler: WKScriptMessageHandler) { self.realHandler = handler }
|
|
700
|
+
func userContentController(_ ucc: WKUserContentController, didReceive m: WKScriptMessage) {
|
|
701
|
+
realHandler?.userContentController(ucc, didReceive: m)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
class WebBridge: NSObject, WKScriptMessageHandler {
|
|
706
|
+
var webView: WKWebView!
|
|
707
|
+
private let weakHandler: WeakScriptMessageHandler
|
|
708
|
+
override init() {
|
|
709
|
+
weakHandler = WeakScriptMessageHandler({} as WKScriptMessageHandler) // placeholder, set below
|
|
710
|
+
super.init()
|
|
711
|
+
weakHandler.realHandler = self
|
|
712
|
+
let cfg = WKWebViewConfiguration()
|
|
713
|
+
cfg.userContentController.add(weakHandler, name: "native") // controller retains the proxy, not self
|
|
714
|
+
webView = WKWebView(frame: .zero, configuration: cfg)
|
|
715
|
+
}
|
|
716
|
+
func userContentController(_: WKUserContentController, didReceive: WKScriptMessage) { /* */ }
|
|
717
|
+
}`,
|
|
718
|
+
notes: "Yes, this is verbose. WeakScriptMessageHandler is a one-time helper that pays for itself across every WKWebView bridge in the app.",
|
|
719
|
+
},
|
|
720
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
721
|
+
// v1.7 catalog
|
|
722
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
723
|
+
"swiftdata.modelcontext-actor-cycle": {
|
|
724
|
+
before: `actor DataLayer {
|
|
725
|
+
let context: ModelContext
|
|
726
|
+
private let executor: DefaultSerialModelExecutor
|
|
727
|
+
init(container: ModelContainer) {
|
|
728
|
+
context = ModelContext(container)
|
|
729
|
+
executor = DefaultSerialModelExecutor(modelContext: context) // ⚠️ cycle
|
|
730
|
+
}
|
|
731
|
+
}`,
|
|
732
|
+
after: `// Prefer the @ModelActor macro (iOS 17+) — handles the executor wiring safely
|
|
733
|
+
@ModelActor
|
|
734
|
+
actor DataLayer {
|
|
735
|
+
func fetchItems() throws -> [Item] {
|
|
736
|
+
try modelContext.fetch(FetchDescriptor<Item>())
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// If you must roll your own, hold ModelContext weakly inside the executor and
|
|
741
|
+
// re-resolve per operation:
|
|
742
|
+
final class WeakContextExecutor {
|
|
743
|
+
weak var context: ModelContext?
|
|
744
|
+
init(_ context: ModelContext) { self.context = context }
|
|
745
|
+
}`,
|
|
746
|
+
notes: "Apple fixed the framework-level shape in iOS 18 beta 1 (FB13844786). When your minimum target is iOS 18+, the @ModelActor-generated executor is safe.",
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
/** Returns the fix template for a given pattern, or null if unknown. */
|
|
750
|
+
export function getFixTemplate(patternId) {
|
|
751
|
+
return TEMPLATES[patternId] ?? null;
|
|
752
|
+
}
|
|
753
|
+
/** All known pattern ids that have templates. Used in tests for coverage assertion. */
|
|
754
|
+
export function knownTemplatePatternIds() {
|
|
755
|
+
return Object.keys(TEMPLATES);
|
|
756
|
+
}
|
|
757
|
+
//# sourceMappingURL=fixTemplates.js.map
|