Skip to content

Add defer_before_unstructured_task rule - #6857

Open
alisher-zinullayev wants to merge 1 commit into
realm:mainfrom
alisher-zinullayev:defer-before-unstructured-task
Open

Add defer_before_unstructured_task rule#6857
alisher-zinullayev wants to merge 1 commit into
realm:mainfrom
alisher-zinullayev:defer-before-unstructured-task

Conversation

@alisher-zinullayev

@alisher-zinullayev alisher-zinullayev commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Implements the rule proposed in #6619: flags a defer block that assigns to
shared state right before a sibling unstructured Task (or Task.detached)
reads that same state.

The deferred assignment runs the instant the enclosing synchronous scope
returns, before the Task's body has a chance to run — inverting the
intended order. The canonical example is a loading flag being reset before
the async work it's guarding has actually finished:

func login() {
    isLoading = true
    defer { isLoading = false }   // <- runs immediately on return
    Task {
        await doSomethingAsync()  // isLoading is already false here
    }
}

Heuristic (opt-in, narrow, false-negative-friendly)

Triggers when all of the following hold, per the shape proposed in the issue:

  • A defer sits in a synchronous function/closure (not async).
  • Its body is one or two simple assignments to identifiers (x = ... or
    self.x = ...), which filters out logging-only defers and lock releases.
  • A sibling statement at the same scope is a discarded Task { ... } or
    Task.detached { ... } call (not captured in a let/var).
  • The Task's trailing closure references at least one of the
    defer-assigned identifiers.

Anything outside this shape does not warn, matching the issue's
false-negative-friendly design goal.

Testing

All triggering/non-triggering examples from the issue (plus a couple extra
covering the two-assignment and captured-Task cases) are included as the
rule's triggeringExamples/nonTriggeringExamples, which make register
turns into generated unit tests.

Test plan

  • swift build
  • swift test --filter DeferBeforeUnstructuredTaskRuleGeneratedTests
  • swift test (full suite, 1089 tests passing)
  • Dogfooded swiftlint lint against the repo itself (0 violations)
  • Manually verified against the issue's positive/negative examples with the built swiftlint binary

Closes #6619

Flags a `defer` block that assigns to shared state right before a sibling
unstructured `Task` (or `Task.detached`) reads that same state. The deferred
assignment runs the instant the enclosing synchronous scope returns, before
the Task's body has a chance to run, inverting the intended order — most
commonly seen as a loading flag being reset before the async work it guards
has actually finished.

Closes realm#6619
@alisher-zinullayev
alisher-zinullayev force-pushed the defer-before-unstructured-task branch from 95c2e97 to 8b1ecf7 Compare August 6, 2026 11:32
@SwiftLintBot

SwiftLintBot commented Aug 6, 2026

Copy link
Copy Markdown
1 Warning
⚠️ This PR introduced a violation in Wire: /wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ConversationListContentController.swift:352:9: Warning: A defer block runs the moment its enclosing synchronous scope returns, before a sibling unstructured Task has a chance to run its body. Assigning to shared state in such a defer while a Task in the same scope reads that state is usually a bug: the state is reset before the asynchronous work it's guarding has actually finished. Move the assignment inside the Task, or make the enclosing function async and await the work directly (defer_before_unstructured_task)
19 Messages
📖 Building this branch resulted in a binary size of 28471.1 KiB vs 28426.13 KiB when built on main (0% larger).
📖 Linting Aerial with this PR took 0.62 s vs 0.64 s on main (3% faster).
📖 Linting Alamofire with this PR took 0.93 s vs 0.91 s on main (2% slower).
📖 Linting Brave with this PR took 5.97 s vs 5.94 s on main (0% slower).
📖 Linting DuckDuckGo with this PR took 26.79 s vs 26.76 s on main (0% slower).
📖 Linting Firefox with this PR took 10.74 s vs 10.75 s on main (0% faster).
📖 Linting Kickstarter with this PR took 6.8 s vs 6.84 s on main (0% faster).
📖 Linting Moya with this PR took 0.37 s vs 0.38 s on main (2% faster).
📖 Linting NetNewsWire with this PR took 2.37 s vs 2.37 s on main (0% slower).
📖 Linting Nimble with this PR took 0.54 s vs 0.53 s on main (1% slower).
📖 Linting PocketCasts with this PR took 6.97 s vs 6.97 s on main (0% slower).
📖 Linting Quick with this PR took 0.37 s vs 0.35 s on main (5% slower).
📖 Linting Realm with this PR took 2.87 s vs 2.9 s on main (1% faster).
📖 Linting Sourcery with this PR took 1.59 s vs 1.61 s on main (1% faster).
📖 Linting Swift with this PR took 4.27 s vs 4.26 s on main (0% slower).
📖 Linting SwiftLintPerformanceTests with this PR took 0.17 s vs 0.17 s on main (0% slower).
📖 Linting VLC with this PR took 1.23 s vs 1.24 s on main (0% faster).
📖 Linting Wire with this PR took 15.45 s vs 15.46 s on main (0% faster).
📖 Linting WordPress with this PR took 9.66 s vs 9.66 s on main (0% slower).

Generated by 🚫 Danger

@alisher-zinullayev

Copy link
Copy Markdown
Author

@SimplyDanny Would you mind taking a look when you have a chance? This implements the defer_before_unstructured_task rule proposed in #6619. All CI checks (bazel, swiftpm, tsan-tests, danger/OSSCheck) are passing. Happy to make any adjustments you'd suggest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rule request: defer_before_unstructured_task

2 participants