dart-lang / linter

Linter for Dart.
https://dart.dev/tools/linter-rules
BSD 3-Clause "New" or "Revised" License
628 stars 170 forks source link

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

Closed navaronbracke closed 4 months ago

navaronbracke commented 5 months ago

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 https://github.com/dart-lang/linter/issues/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

bwilkerson commented 5 months ago

@goderbauer

goderbauer commented 5 months ago

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

srawlins commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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

srawlins commented 4 months ago

Fixed with https://github.com/dart-lang/sdk/commit/a6762dd24c208f78a956049f22b155be4c25a77e