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.
Files changed (45) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +76 -34
  3. package/USAGE.md +112 -41
  4. package/dist/index.js +43 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/axe.d.ts +86 -0
  7. package/dist/runtime/axe.js +249 -0
  8. package/dist/runtime/axe.js.map +1 -0
  9. package/dist/runtime/buildSettings.d.ts +27 -0
  10. package/dist/runtime/buildSettings.js +88 -0
  11. package/dist/runtime/buildSettings.js.map +1 -0
  12. package/dist/runtime/exec.d.ts +8 -1
  13. package/dist/runtime/exec.js +8 -2
  14. package/dist/runtime/exec.js.map +1 -1
  15. package/dist/runtime/fixTemplates.d.ts +27 -0
  16. package/dist/runtime/fixTemplates.js +757 -0
  17. package/dist/runtime/fixTemplates.js.map +1 -0
  18. package/dist/runtime/simctl.d.ts +68 -0
  19. package/dist/runtime/simctl.js +194 -0
  20. package/dist/runtime/simctl.js.map +1 -0
  21. package/dist/runtime/staticAnalysisHints.js +8 -0
  22. package/dist/runtime/staticAnalysisHints.js.map +1 -1
  23. package/dist/tools/bootAndLaunchForLeakInvestigation.d.ts +166 -0
  24. package/dist/tools/bootAndLaunchForLeakInvestigation.js +367 -0
  25. package/dist/tools/bootAndLaunchForLeakInvestigation.js.map +1 -0
  26. package/dist/tools/captureMemgraph.d.ts +29 -1
  27. package/dist/tools/captureMemgraph.js +148 -6
  28. package/dist/tools/captureMemgraph.js.map +1 -1
  29. package/dist/tools/captureScenarioState.d.ts +77 -0
  30. package/dist/tools/captureScenarioState.js +159 -0
  31. package/dist/tools/captureScenarioState.js.map +1 -0
  32. package/dist/tools/classifyCycle.d.ts +7 -0
  33. package/dist/tools/classifyCycle.js +31 -0
  34. package/dist/tools/classifyCycle.js.map +1 -1
  35. package/dist/tools/compareTracesByPattern.d.ts +112 -0
  36. package/dist/tools/compareTracesByPattern.js +312 -0
  37. package/dist/tools/compareTracesByPattern.js.map +1 -0
  38. package/dist/tools/detectLeaksInXCUITest.d.ts +2 -2
  39. package/dist/tools/getInvestigationPlaybook.d.ts +15 -0
  40. package/dist/tools/getInvestigationPlaybook.js +24 -1
  41. package/dist/tools/getInvestigationPlaybook.js.map +1 -1
  42. package/dist/tools/replayScenario.d.ts +243 -0
  43. package/dist/tools/replayScenario.js +187 -0
  44. package/dist/tools/replayScenario.js.map +1 -0
  45. 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