Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

Commit fa61a59

Browse files
authored
fix: additional cases for use-setstate-synchronously (#1136)
* feat: scan try-statements for async setState * feat: scan switch-statements for async setState * test: control flow in for- and while-statements * docs: mention new heuristics * chore: update changelog
1 parent e3d2fcf commit fa61a59

File tree

7 files changed

+206
-11
lines changed

7 files changed

+206
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
* fix: handle try and switch statements for [`use-setstate-synchronously`](https://dartcodemetrics.dev/docs/rules/flutter/use-setstate-synchronously)
6+
37
## 5.4.0
48

59
* feat: ignore tear-off methods for [`avoid-unused-parameters`](https://dartcodemetrics.dev/docs/rules/common/avoid-unused-parameters).

lib/src/analyzers/lint_analyzer/rules/rules_list/use_setstate_synchronously/helpers.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ bool _isIdentifier(Expression node, String ident) =>
8585
node is Identifier && node.name == ident;
8686

8787
@pragma('vm:prefer-inline')
88-
bool _isDivergent(Statement node) =>
88+
bool _isDivergent(Statement node, {bool allowControlFlow = false}) =>
8989
node is ReturnStatement ||
90+
allowControlFlow && (node is BreakStatement || node is ContinueStatement) ||
9091
node is ExpressionStatement && node.expression is ThrowExpression;
9192

9293
@pragma('vm:prefer-inline')
@@ -95,5 +96,12 @@ Expression _thisOr(Expression node) =>
9596
? node.propertyName
9697
: node;
9798

98-
bool _blockDiverges(Statement block) =>
99-
block is Block ? block.statements.any(_isDivergent) : _isDivergent(block);
99+
bool _blockDiverges(Statement block, {required bool allowControlFlow}) => block
100+
is Block
101+
? block.statements
102+
.any((node) => _isDivergent(node, allowControlFlow: allowControlFlow))
103+
: _isDivergent(block, allowControlFlow: allowControlFlow);
104+
105+
bool _caseDiverges(SwitchMember arm, {required bool allowControlFlow}) =>
106+
arm.statements
107+
.any((node) => _isDivergent(node, allowControlFlow: allowControlFlow));

lib/src/analyzers/lint_analyzer/rules/rules_list/use_setstate_synchronously/visitor.dart

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ class _AsyncSetStateVisitor extends RecursiveAstVisitor<void> {
3131
_AsyncSetStateVisitor({this.validateMethod = _noop});
3232

3333
MountedFact mounted = true.asFact();
34+
bool inControlFlow = false;
3435
bool inAsync = true;
36+
37+
bool get isMounted => mounted.value ?? false;
3538
final nodes = <SimpleIdentifier>[];
3639

3740
@override
@@ -47,8 +50,7 @@ class _AsyncSetStateVisitor extends RecursiveAstVisitor<void> {
4750
}
4851

4952
// [this.]setState()
50-
final mounted_ = mounted.value ?? false;
51-
if (!mounted_ &&
53+
if (!isMounted &&
5254
validateMethod(node.methodName.name) &&
5355
node.target is ThisExpression?) {
5456
nodes.add(node.methodName);
@@ -74,12 +76,15 @@ class _AsyncSetStateVisitor extends RecursiveAstVisitor<void> {
7476
var elseDiverges = false;
7577
final elseStatement = node.elseStatement;
7678
if (elseStatement != null) {
77-
elseDiverges = _blockDiverges(elseStatement);
79+
elseDiverges = _blockDiverges(
80+
elseStatement,
81+
allowControlFlow: inControlFlow,
82+
);
7883
mounted = _tryInvert(newMounted).or(mounted);
7984
elseStatement.visitChildren(this);
8085
}
8186

82-
if (_blockDiverges(node.thenStatement)) {
87+
if (_blockDiverges(node.thenStatement, allowControlFlow: inControlFlow)) {
8388
mounted = _tryInvert(newMounted).or(beforeThen);
8489
} else if (elseDiverges) {
8590
mounted = beforeThen != afterThen
@@ -95,14 +100,35 @@ class _AsyncSetStateVisitor extends RecursiveAstVisitor<void> {
95100
}
96101

97102
node.condition.visitChildren(this);
103+
98104
final oldMounted = mounted;
99105
final newMounted = _extractMountedCheck(node.condition);
100106
mounted = newMounted.or(mounted);
107+
final oldInControlFlow = inControlFlow;
108+
inControlFlow = true;
101109
node.body.visitChildren(this);
102110

103-
if (_blockDiverges(node.body)) {
111+
if (_blockDiverges(node.body, allowControlFlow: inControlFlow)) {
104112
mounted = _tryInvert(newMounted).or(oldMounted);
105113
}
114+
115+
inControlFlow = oldInControlFlow;
116+
}
117+
118+
@override
119+
void visitForStatement(ForStatement node) {
120+
if (!inAsync) {
121+
return node.visitChildren(this);
122+
}
123+
124+
node.forLoopParts.visitChildren(this);
125+
126+
final oldInControlFlow = inControlFlow;
127+
inControlFlow = true;
128+
129+
node.body.visitChildren(this);
130+
131+
inControlFlow = oldInControlFlow;
106132
}
107133

108134
@override
@@ -117,4 +143,57 @@ class _AsyncSetStateVisitor extends RecursiveAstVisitor<void> {
117143
mounted = oldMounted;
118144
inAsync = oldInAsync;
119145
}
146+
147+
@override
148+
void visitTryStatement(TryStatement node) {
149+
if (!inAsync) {
150+
return node.visitChildren(this);
151+
}
152+
153+
final oldMounted = mounted;
154+
node.body.visitChildren(this);
155+
final afterBody = mounted;
156+
// ignore: omit_local_variable_types
157+
final MountedFact beforeCatch =
158+
mounted == oldMounted ? oldMounted : false.asFact();
159+
for (final clause in node.catchClauses) {
160+
mounted = beforeCatch;
161+
clause.visitChildren(this);
162+
}
163+
164+
final finallyBlock = node.finallyBlock;
165+
if (finallyBlock != null) {
166+
mounted = beforeCatch;
167+
finallyBlock.visitChildren(this);
168+
} else {
169+
mounted = afterBody;
170+
}
171+
}
172+
173+
@override
174+
void visitSwitchStatement(SwitchStatement node) {
175+
if (!inAsync) {
176+
return node.visitChildren(this);
177+
}
178+
179+
node.expression.visitChildren(this);
180+
181+
final oldInControlFlow = inControlFlow;
182+
inControlFlow = true;
183+
184+
final caseInvariant = mounted;
185+
for (final arm in node.members) {
186+
arm.visitChildren(this);
187+
if (mounted != caseInvariant &&
188+
!_caseDiverges(arm, allowControlFlow: false)) {
189+
mounted = false.asFact();
190+
}
191+
192+
if (_caseDiverges(arm, allowControlFlow: true)) {
193+
mounted = caseInvariant;
194+
}
195+
}
196+
197+
inControlFlow = oldInControlFlow;
198+
}
120199
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
class MyState extends State {
2+
void tryStatements() async {
3+
try {
4+
await doStuff();
5+
} on FooException {
6+
if (!mounted) return;
7+
setState(() {});
8+
} on BarException {
9+
setState(() {}); // LINT
10+
}
11+
setState(() {}); // LINT
12+
}
13+
14+
void tryFinally() async {
15+
try {
16+
await doStuff();
17+
} on Exception {
18+
await doStuff();
19+
} finally {
20+
if (!mounted) return;
21+
}
22+
setState(() {});
23+
}
24+
25+
void switchStatements() async {
26+
await doStuff();
27+
switch (foobar) {
28+
case 'foo':
29+
if (!mounted) break;
30+
setState(() {});
31+
break;
32+
case 'bar':
33+
setState(() {}); // LINT
34+
break;
35+
case 'baz':
36+
await doStuff();
37+
break;
38+
}
39+
setState(() {}); // LINT
40+
}
41+
42+
void switchAsync() async {
43+
switch (foobar) {
44+
case 'foo':
45+
await doStuff();
46+
return;
47+
}
48+
setState(() {});
49+
}
50+
}
51+
52+
class State {}

test/src/analyzers/lint_analyzer/rules/rules_list/use_setstate_synchronously/examples/known_errors.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ class _MyState extends State {
88
doStuff();
99
setState(() {});
1010
}
11+
12+
void controlFlow() {
13+
await doStuff();
14+
for (;;) {
15+
if (!mounted) break;
16+
setState(() {});
17+
18+
await doStuff();
19+
if (!mounted) continue;
20+
setState(() {});
21+
}
22+
23+
await doStuff();
24+
while (true) {
25+
if (!mounted) break;
26+
27+
setState(() {});
28+
}
29+
}
1130
}
1231

1332
class State {}

test/src/analyzers/lint_analyzer/rules/rules_list/use_setstate_synchronously/use_setstate_synchronously_rule_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import '../../../../../helpers/rule_test_helper.dart';
66

77
const _examplePath = 'use_setstate_synchronously/examples/example.dart';
88
const _issuesPath = 'use_setstate_synchronously/examples/known_errors.dart';
9+
const _trySwitchPath =
10+
'use_setstate_synchronously/examples/extras_try_switch.dart';
911

1012
void main() {
1113
group('UseSetStateSynchronouslyTest', () {
@@ -90,5 +92,28 @@ void main() {
9092
],
9193
);
9294
});
95+
96+
test('reports issues with try- and switch-statements', () async {
97+
final unit = await RuleTestHelper.resolveFromFile(_trySwitchPath);
98+
final issues = UseSetStateSynchronouslyRule().check(unit);
99+
100+
RuleTestHelper.verifyIssues(
101+
issues: issues,
102+
startLines: [9, 11, 33, 39],
103+
startColumns: [7, 5, 9, 5],
104+
locationTexts: [
105+
'setState',
106+
'setState',
107+
'setState',
108+
'setState',
109+
],
110+
messages: [
111+
"Avoid calling 'setState' past an await point without checking if the widget is mounted.",
112+
"Avoid calling 'setState' past an await point without checking if the widget is mounted.",
113+
"Avoid calling 'setState' past an await point without checking if the widget is mounted.",
114+
"Avoid calling 'setState' past an await point without checking if the widget is mounted.",
115+
],
116+
);
117+
});
93118
});
94119
}

website/docs/rules/flutter/use-setstate-synchronously.mdx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ does not make use of [`mounted`], but rather revolves around refactoring your st
1616

1717
:::info
1818

19-
Only these patterns are considered for mounted-ness:
19+
The following patterns are recognized when statically determining mountedness:
2020

2121
- `if (mounted)`
22-
- `if (!mounted)`
2322
- `if (mounted && ..)`
24-
- `if (!mounted || ..) return;`
23+
- `if (!mounted || ..)`
24+
- `try` and `switch` mountedness per branch
25+
- Divergence in `for`, `while` and `switch` statements using `break` or `continue`
2526

2627
If a `!mounted` check diverges, i.e. ends in a `return` or `throw`, the outer scope is considered mounted and vice versa:
2728

@@ -32,6 +33,13 @@ setState(() { ... });
3233
3334
// After this statement, need to check 'mounted' again
3435
await fetch(...);
36+
37+
// In control flow statements, 'break' and 'continue' diverges
38+
while (...) {
39+
if (!mounted) break;
40+
// Should be mounted right now
41+
setState(() { ... });
42+
}
3543
```
3644

3745
:::

0 commit comments

Comments
 (0)