Rule request: `defer_before_unstructured_task`
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 19.7k
- Forks
- 2.3k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 11
Description
Pattern
In a synchronous function, defer { x = ... } followed by Task { await ... ; ... uses x ... } is a recurring bug. The deferred change runs the moment the function returns (before the Task body has a chance to run), so the cleanup is observed inverted from intent.
Canonical example (loading-flag management in SwiftUI view models):
func login() {
isLoading = true
defer { isLoading = false }
Task {
await doSomethingAsync()
}
}
isLoading flips to false immediately when login() returns, while doSomethingAsync() is still running.
Why a rule
- The pattern is widespread in tutorials, SwiftUI sample code, and production. Folklore-known but not flagged anywhere.
- The compiler does not warn (verified against current main).
- SwiftLint already has
unhandled_throwing_taskandinert_defer. This rule sits naturally between them. - SE-0493 (async-defer) and SE-0520 (discardable Task) are the relevant Swift Evolution context, neither addresses this shape.
Proposed heuristic (narrow, opt-in)
Trigger when ALL hold:
DeferStmtinside a synchronous function or closure.- The defer body contains one or two simple assignments to identifiers (filters out logging-only defers and lock release).
- A sibling statement at the same scope is a discarded
Task { ... }orTask.detached { ... }initializer. - The Task's trailing closure references at least one of the identifiers assigned in the defer.
Anything outside this shape does not warn. False-negative-friendly by design.
Negative examples (must NOT warn)
func f() async { // async + await directly: correct
isLoading = true
defer { isLoading = false }
await work()
}
func f() { // logging-only defer
defer { print("done") }
Task { await work() }
}
func f() { // Task is captured
let t = Task { await work() }
defer { print("leaving") }
_ = t
}
func f() { // no shared state
var localFlag = true
defer { localFlag = false }
Task { await work() }
_ = localFlag
}
Positive examples (must warn)
func login() {
isLoading = true
defer { isLoading = false } // <- triggers
Task {
await doSomethingAsync()
_ = isLoading
}
}
func login() {
isLoading = true
defer { isLoading = false } // <- triggers
Task.detached {
await doSomethingAsync()
_ = await self.isLoading
}
}
Suggested fix-its (textual)
- Move the
deferinside theTaskclosure. - Make the enclosing function
asyncandawaitthe work directly.
Reference template
Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swift is structurally near-identical (recognizes Task initializers and inspects their trailing closures).
A working sketch is in this repo at swiftlint-rule/DeferBeforeUnstructuredTaskRule.swift.
Severity / opt-in
Default warning, opt-in. After adoption data accumulates, the heuristic could feed a Swift Forums pitch for a compiler-side diagnostic.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swift and compare it with the working sketch at swiftlint-rule/DeferBeforeUnstructuredTaskRule.swift. Use the listed positive and negative examples to verify the narrow heuristic, and finish with a warning-level opt-in rule that avoids the specified false positives.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- tooling
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100