Skip to content
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

proposal: use_build_context_synchronously should lint unsafe usages of sync then(), catchError() and whenComplete() callbacks #4923

Closed
5 tasks done
navaronbracke opened this issue Apr 10, 2024 · 8 comments
Assignees
Labels
lint-proposal set-flutter Affects a rule in the recommended Flutter rule set status-pending type-enhancement A request for a change that isn't a bug

Comments

@navaronbracke
Copy link

navaronbracke commented Apr 10, 2024

use_build_context_synchronously

Description

"Don't use BuildContexts across async gaps inside Future callbacks" (This wording can definitely be better)

Details

Currently the use_build_context_synchronously warns if a BuildContext is used after an await, without a mounted check on said context. The same rule also applies to uses inside Future.then(), Future.catchError() and Future.whenComplete(), but only if said functions use awaits themselves.

A developer could write code using the then/catchError/whenComplete callbacks and not use any async/await keyword at all, but the underlying rule still applies.

I filed #4892 in the past, where I did mention the callbacks as well, but for setState((){}) uses.

Kind

This enhancement would guard against errors.

Bad Examples

Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).then((int value) {
 Navigator.of(context).pop();
});
Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).catchError((Object error, StackTrace stackTrace) {
 Navigator.of(context).pop();
});
Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).whenComplete(() {
 Navigator.of(context).pop();
});

Good Examples

Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).then((int value) {
 if (!context.mounted) return;

 Navigator.of(context).pop();
});
Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).catchError((Object error, StackTrace stackTrace) {
 if (!context.mounted) return;

 Navigator.of(context).pop();
});
Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).whenComplete(() {
 if (!context.mounted) return;

 Navigator.of(context).pop();
});

Discussion

Add any other motivation or useful context here.

Discussion checklist

  • List any existing rules this proposal modifies, complements, overlaps or conflicts with.
  • List any relevant issues (reported here, the SDK Tracker, or elsewhere).
  • If there's any prior art (e.g., in other linters), please add references here.
  • If this proposal corresponds to Effective Dart or Flutter Style Guide advice, please call it out. (If there isn't any corresponding advice, should there be?)
  • If this proposal is motivated by real-world examples, please provide as many details as you can. Demonstrating potential impact is especially valuable.
@github-actions github-actions bot added the set-flutter Affects a rule in the recommended Flutter rule set label Apr 10, 2024
@navaronbracke navaronbracke changed the title proposal: use_build_context_synchronously should lint unsafe usages inside then(), catchError() and whenComplete() proposal: use_build_context_synchronously should lint unsafe usages of sync then(), catchError() and whenComplete() callbacks Apr 10, 2024
@bwilkerson
Copy link
Member

@goderbauer

@goderbauer
Copy link
Contributor

I agree with the issue author. Ideally, we'd warn in those cases as well.

@srawlins srawlins added the type-enhancement A request for a change that isn't a bug label Apr 12, 2024
@srawlins
Copy link
Member

What is the triggering condition here? A function literal inside a Future.then callback? Plus a few others? What about a function otherwise:

void f(int value) {
 Navigator.of(context).pop();
}

Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).then(f);

In static analysis we cannot track how functions are called through an arbitrary number of functions. It would not be hard to just trigger on function literals passed to Future.then (plus a few others), but very hard to track arbitrary functions being passed to Future.then. Is the former enough?

@navaronbracke
Copy link
Author

It would not be hard to just trigger on function literals passed to Future.then (plus a few others)

This would already be a big step in the right direction.

@goderbauer
Copy link
Contributor

It would not be hard to just trigger on function literals passed to Future.then (plus a few others), but very hard to track arbitrary functions being passed to Future.then. Is the former enough?

That's similar to how the rule works today for the async/await case, right? For example, in the following code you will not get any lints on the problematic use of context inside the methodThatWillUseContext?

await future;
methodThatWillUseContext();

So, I think just warning on this in function literals would be a good idea and match what we do in other cases.

@srawlins
Copy link
Member

That's similar to how the rule works today for the async/await case, right?

That's right. I hadn't thought about how this request mirrors the existing code pretty well, and the same limitation would apply. This should be technically simple, but we'll see where the dragons lie when applying it to the flutter repos :D

@srawlins
Copy link
Member

srawlins commented May 2, 2024

This could potentially apply to gobs of Dart SDK function callbacks. Here's what I've got so far:

  • new Future, as the first positional argument,
  • new Future.delayed, as the second positional argument,
  • new Future.microtask, as the first positional argument,
  • Future.catchError, as the first positional argument, or the test
    named argument,
  • Future.onError, as the first positional argument, or the test
    named argument,
  • Future.then, as the first positional argument, or the onError named
    argument,
  • Future.timeout, as the onTimeout named argument,
  • Future.whenComplete, as the first positional argument,
  • Future.doWhile, as the first positional argument,
  • Future.forEach, as the second positional argument,
  • Future.wait, as the cleanUp named argument,
  • new Stream.multi, as the first positional argument,
  • new Stream.periodic, as the second positional argument,
  • Stream.any, as the first positional argument,
  • wow many more Stream methods...

@srawlins srawlins self-assigned this May 6, 2024
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue May 10, 2024
Fixes dart-lang/linter#4923

In this change we now protect code inside callback arguments to
functions like `Future.then`. For example:

```dart
Future<int>.delayed(Duration(seconds: 1), (_) {
  return 42;
}).then((int value) {
 Navigator.of(context).pop();
});
```

In code like this, each reference to BuildContext must be guarded by
a mounted check.

There are a number of "protected functions" that the rule now checks:

Constructors:
* Future.new: 1st positional parameter
* Future.delayed: 2nd positional parameter
* Future.microtask: 1st positional parameter
* Stream.eventTransformed: 2nd positional parameter
* Stream.multi: 1st positional parameter
* Stream.periodic: 2nd positional parameter

* StreamController.new: 'onListen', 'onPause', 'onResume', and
  'onCancel' named parameters
* StreamController.broadcast: 'onListen' and 'onCancel' named
  parameters

Instance Methods:
* Future.catchError: first positional parameter, and 'test' named
  parameter
* Future.onError: 1st positional parameter, and 'test' named parameter
* Future.then: 1st positional parameter, and 'onError' named
  parameter
* Future.timeout: 'onTimeout' named parameter
* Future.whenComplete: 1st positional parameter
* Stream.any: 1st positional parameter
* Stream.asBroadcastStream: 'onListen' and 'onCancel' named parameters
* Stream.asyncExpand: 1st positional parameter
* Stream.asyncMap: 1st positional parameter
* Stream.distinct: 1st positional parameter
* Stream.expand: 1st positional parameter
* Stream.firstWhere: first positional parameter, and 'orElse' named
  parameter
* Stream.fold: second positional parameter
* Stream.forEach: 1st positional parameter
* Stream.handleError: 1st positional parameter, and 'test' named
  parameter
* Stream.lastWhere: 1st positional parameter, and 'orElse' named
  parameter
* Stream.listen: 1st positional parameter, and 'onError' and 'onDone'
  named parameters
* Stream.map: 1st positional parameter
* Stream.reduce: 1st positional parameter
* Stream.singleWhere: 1st positional parameter, and 'orElse' named
  parameter
* Stream.skipWhile: 1st positional parameter
* Stream.takeWhile: 1st positional parameter
* Stream.timeout: 'onTimeout' named parameter
* Stream.where: 1st positional parameter
* Stream.onData: 1st positional parameter
* Stream.onDone: 1st positional parameter
* Stream.onError: 1st positional parameter

Static Methods:
* Future.doWhile: 1st positional parameter
* Future.forEach: 2nd positional parameter
* Future.wait: 'cleanUp' named parameter



Change-Id: I703d5cfbee5e8af1f703f40046832d66b9b59bc2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365541
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
@srawlins
Copy link
Member

Fixed with dart-lang/sdk@a6762dd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lint-proposal set-flutter Affects a rule in the recommended Flutter rule set status-pending type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

4 participants