8000 [Sema]: diagnose implicit strong captures of weak capture list entries by jamieQ · Pull Request #77063 · swiftlang/swift · GitHub
[go: up one dir, main page]

Skip to content

[Sema]: diagnose implicit strong captures of weak capture list entries #77063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

jamieQ
Copy link
Contributor
@jamieQ jamieQ commented Oct 17, 2024

Add a diagnostic to warn when a capture list item creates a weak or unowned binding to a value with strong ownership that is captured in an outer escaping closure, but is not itself in a capture list.


This change adds a new diagnostic for situations like the following:

class SomeViewController: UIViewController {
  // ...
  func foo(request: URLRequest) {
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
      let result = // process data...
      DispatchQueue.main.async { [weak self] in // capture list entry causes `self` to be strongly captured in the parent closure scope
        self?.handleResult(result)
      }
    }
    task.
8000
resume()
  }
}

the goal is to emit warnings for such cases along the lines of:

example.swift:10:40: warning: 'weak' capture list item 'self' implicitly captured as strong reference in outer scope
  6 |   // ...
  7 |   func foo(request: URLRequest) {
  8 |     let task = URLSession.shared.dataTask(with: request) { data, response, error in
    |                                                          `- note: 'self' implicitly captured here
  9 |       let result = // process data...
 10 |       DispatchQueue.main.async { [weak self] in
    |                                        `- warning: 'weak' capture list item 'self' implicitly captured as strong reference in outer scope
 11 |         self?.handleResult(result)
 12 |       }

forum post regarding this change: https://forums.swift.org/t/request-for-feedback-emitting-warnings-when-weak-capture-list-items-cause-strong-captures/78390


Changing this behavior or adding a warning has been brought up many times over the years. I found the following existing references to this issue:

  1. An old mailing list proposal
  2. [SR-3805] Creating a strong reference to self in a closure should warn if self is never accessed "strongly" #46390
  3. [SR-4893] [weak self] leaks #47470
  4. Using [weak self] in nested closures results in strong capture without warning #72391
  5. This recent blog post about a memory leak

@jamieQ

This comment was marked as resolved.

@jamieQ jamieQ marked this pull request as ready for review October 17, 2024 12:16
@jamieQ

This comment was marked as resolved.

@jamieQ

This comment was marked as resolved.

@hamishknight hamishknight self-requested a review November 21, 2024 17:15
@xedin
Copy link
Contributor
xedin commented Nov 21, 2024

Sorry for the delay, @hamishknight is going to take a look at the changes soon!

Copy link
Contributor
@hamishknight hamishknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good start, thank you for taking this on! I think this should probably be moved to MiscDiagnostics

Comment on lines 270 to 273
Context.Diags.diagnose(weakifiedRef->getLoc(),
diag::implicit_nonstrong_to_strong_capture);
Context.Diags.diagnose(CaptureLoc,
diag::implicit_nonstrong_to_strong_capture_loc);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also ought to have a fix-it to explicitly specify the strong capture to silence the diagnostic (we could also suggest moving the weak capture to the outer closure). For a local func, I suppose we could suggest explicitly rebinding the capture (i.e let x = x). That is a bit ugly for self though (i.e you'd need to let `self` = self

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a couple follow-up questions here:

regarding the fix-its – do you think having a fix-it is a blocking requirement for adding a diagnostic like this, or could it be done as a follow-up task? i'm not trying to be lazy, but the 'proper' solution to offer seems like it would be to splice in a weak capture list item into a parent closure (and potentially multiple of them...), and i don't have much domain knowledge about how to do that. offering to make the capture strong seems more straightforward (just delete some known source range), but also seems like the 'wrong' thing to push people towards, given the contexts in which i assume this diagnostic would actually be useful.

re: the local func suggestion – i don't follow the scenario you're alluding to here. would you mind spelling it out more explicitly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy for the fix-it to be done as a follow-up, it doesn't have to block this change

For the local function case I was thinking of something like:

class C {
  func foo() {
    func bar() { // implicit strong capture here
      _ = { [weak self] in }
    }
    takesEscapingFn(bar)
  }
}

Although that being said, we don't currently diagnose the implicit use of self in such local functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i'd prefer to split out fix-its as a distinct effort then, though if you have ideas for how the diagnostic wording could be changed to suggest a solution i'd be glad to hear them (tack some extra text onto the warning or note?).

in terms of the local funcs... i'm not quite sure what to do there. since they are sort of ignored in terms of other self-capture diagnostics, i'm inclined to at least start by mostly ignoring them. with the current implementation, we get the following (which is perhaps a sort of odd pattern – a local func in a closure):

func localFunc() {
  escape {
         `- note: 'self' implicitly captured here
    func localFunc2() {
      escape { [weak self] in }
                     `- warning: 'weak' capture implicitly captures 'self' as strong reference in outer scope
      _ = self
    }
  }
}

which is maybe a bit weird. we currently get no diagnostic in the example you provided. maybe the diagnostic walker could consider func decls in addition to closures?

@jamieQ jamieQ marked this pull request as draft January 21, 2025 13:02
@jamieQ jamieQ force-pushed the jquadri/warn-weak-to-strong-capture branch 3 times, most recently from 0b3d1c9 to 47bf774 Compare March 9, 2025 03:23
@jamieQ
Copy link
Contributor Author
jamieQ commented Mar 9, 2025

@swift-ci please smoke test

@jamieQ jamieQ force-pushed the jquadri/warn-weak-to-strong-capture branch 2 times, most recently from 4a6f35e to 5d5a8a6 Compare March 9, 2025 11:49
Copy link
Contributor Author
@jamieQ jamieQ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hamishknight i think i addressed all your comments from the initial review (thanks!) – the unresolved ones have some clarifying questions from my end. i've updated the implementation to run as an expression pass per your suggestion, and i think the algorithm is more appropriate now as well.

i've left some more 'TODO' comments in the code which i'd appreciate your insights on, as well as a few PR comments/questions of my own. when you get a chance, another pass on the updated code would be great – thanks in advance!

@jamieQ jamieQ marked this pull request as ready for review March 10, 2025 11:44
@hamishknight
Copy link
Contributor

Sorry I didn't get through the rest of this today, I'll try and get to it before the end of the week

@jamieQ
Copy link
Contributor Author
jamieQ commented Mar 19, 2025

@hamishknight if you have time to take another look at this, i'd love to hear the rest of your feedback & continue iterating on it

@hamishknight
Copy link
Contributor

Sorry! I haven't forgotten about this, I will try again to get to it before the end of the week

@jamieQ
Copy 67ED link
Contributor Author
jamieQ commented Mar 28, 2025

@hamishknight i assume you've likely been fairly busy with 6.1 preparations, but any chance you've had time to take another look at this? if not, and you don't foresee having capacity to complete a full review soon, it would be helpful to get guidance on at least the following questions so that i could begin to address them sooner rather than later:

  1. should the diagnostic should be performed via multiple AST traversals rather than one? (outlined here)
  2. is adding a fixit of some form a blocker for this proposed change? (mentioned here)

@hamishknight
Copy link
Contributor

So sorry again! I want to try and get through this PR this week

jamieQ added 5 commits May 3, 2025 13:25
Add a diagnostic to warn when a capture list item creates a weak or
unowned binding to a strong Decl and in so doing induces an implicit
strong capture of the Decl in a parent closure scope.
@jamieQ jamieQ force-pushed the jquadri/warn-weak-to-strong-capture branch from 5d5a8a6 to d084873 Compare May 23, 2025 13:39
@jamieQ
Copy link
Contributor Author
jamieQ commented May 23, 2025

@swift-ci please smoke test macos

@jamieQ jamieQ force-pushed the jquadri/warn-weak-to-strong-capture branch from f130230 to 34cc9e3 Compare May 25, 2025 22:42
Copy link
Contributor Author
@jamieQ jamieQ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hamishknight i made another few passes to update things per your most recent feedback, and the logic for the diagnostic is quite a bit simpler now (IMO). i also added a few more tests with some additional 'TODO' comments that i'd be interested if you have any insights on. thanks in advance!

}
}

// TODO: how to deal with @_implicitSelfCapture and isolated captures?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some additional context/thoughts on this:

the first is whether the implicit self capture annotation should interact in some way with any of this new diagnostic logic.

the second is that, today, moving an isolated parameter capture from being closed over implicitly (due to a reference within the closure) to being an explicit capture list item can change the inferred isolation of the closure. e.g.

func f(iso: isolated (any Actor)) async {
  let x = {
    iso.assertIsolated("x fail") // ✅
    print("x passed")
    await Task.yield()
  }
  let y = { [iso] in
    iso.assertIsolated("y fail") // 🛑
    print("y passed")
    await Task.yield()
  }

  await x()
  await y()
}

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.

3 participants
0