Weak Self In Closure
ID |
swift.weak_self_in_closure |
Severity |
low |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Swift |
Tags |
memory-leak, reliability, suspicious-code |
Description
Reports a closure that is passed to a known-escaping Cocoa / Foundation
API and captures self strongly. The closure outlives the call, so a
strong capture pins self alive at least until the closure runs. When
the closure (or the work it kicks off) is also retained by self, the
two references form a cycle that ARC cannot break.
Task {
self.refresh() // FLAW — strong self
}
DispatchQueue.main.async {
self.update() // FLAW — strong self
}
Task { [weak self] in
self?.refresh() // OK
}
Task { [unowned self] in
self.refresh() // OK
}
Rationale
Closures retain captured references strongly by default. The
allow-list of APIs the rule looks at — Task, DispatchQueue.async,
DispatchQueue.asyncAfter, URLSession.dataTask / downloadTask /
uploadTask, schedule — store their closure for later execution, so
the captured self survives past the enclosing function. Combined
with a closure that is itself held by self (a stored property, a
delegate handler, a subscription) the chain forms a retain cycle. The
process keeps the object graph alive for the lifetime of the program.
The rule scopes conservatively to that fixed allow-list. Precise
escaping-closure detection requires @escaping attribute resolution
across module boundaries, which is not available in the public AST.
The allow-list catches the common offenders without the false
positives of a blanket "closure references `self`" check.
Remediation
Add a capture list that holds self weakly (the usual choice) or
unowned when self is guaranteed to outlive the closure:
Task { [weak self] in
self?.refresh()
}
DispatchQueue.main.async { [unowned self] in
self.update()
}
If the closure body does not need self, refactor so it does not
reference it at all.