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

Commit ea9eaad

Browse files
authored
feat: add use-setstate-synchronously rule (#1120)
* feat: add avoid-async-setstate rule * fix: avoid unnecessary work * fix: reword documentation * fix: unabbreviate variables * feat: and-chain for use-setstate-synchronously * chore: rename rule to use-setstate-synchronously * feat: or-chains for use-setstate-synchronously * fix: spurious false positives * fix: skip analyzing sync blocks * feat: method config for use-setstate-synchronously * chore: report known lint issues * test: known issues, config
1 parent cc1bb04 commit ea9eaad

File tree

13 files changed

+667
-0
lines changed

13 files changed

+667
-0
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+
* feat: add [`use-setstate-synchronously`](https://dartcodemetrics.dev/docs/rules/flutter/use-setstate-synchronously)
6+
37
## 5.3.0
48

59
* feat: add static code diagnostic [`list-all-equatable-fields`](https://dartcodemetrics.dev/docs/rules/common/list-all-equatable-fields).

lib/presets/flutter_all.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dart_code_metrics:
77
- avoid-unnecessary-setstate
88
- avoid-expanded-as-spacer
99
- avoid-wrapping-in-padding
10+
- use-setstate-synchronously
1011
- check-for-equals-in-render-object-setters
1112
- consistent-update-render-object
1213
- prefer-const-border-radius

lib/src/analyzers/lint_analyzer/rules/rules_factory.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import 'rules_list/prefer_trailing_comma/prefer_trailing_comma_rule.dart';
7171
import 'rules_list/prefer_using_list_view/prefer_using_list_view_rule.dart';
7272
import 'rules_list/provide_correct_intl_args/provide_correct_intl_args_rule.dart';
7373
import 'rules_list/tag_name/tag_name_rule.dart';
74+
import 'rules_list/use_setstate_synchronously/use_setstate_synchronously_rule.dart';
7475

7576
final _implementedRules = <String, Rule Function(Map<String, Object>)>{
7677
AlwaysRemoveListenerRule.ruleId: AlwaysRemoveListenerRule.new,
@@ -102,6 +103,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
102103
AvoidTopLevelMembersInTestsRule.ruleId: AvoidTopLevelMembersInTestsRule.new,
103104
AvoidUnnecessaryConditionalsRule.ruleId: AvoidUnnecessaryConditionalsRule.new,
104105
AvoidUnnecessarySetStateRule.ruleId: AvoidUnnecessarySetStateRule.new,
106+
UseSetStateSynchronouslyRule.ruleId: UseSetStateSynchronouslyRule.new,
105107
AvoidUnnecessaryTypeAssertionsRule.ruleId:
106108
AvoidUnnecessaryTypeAssertionsRule.new,
107109
AvoidUnnecessaryTypeCastsRule.ruleId: AvoidUnnecessaryTypeCastsRule.new,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
part of 'use_setstate_synchronously_rule.dart';
2+
3+
Set<String> readMethods(Map<String, Object> options) {
4+
final methods = options['methods'];
5+
6+
return methods != null && methods is Iterable
7+
? methods.whereType<String>().toSet()
8+
: {'setState'};
9+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
part of 'use_setstate_synchronously_rule.dart';
2+
3+
/// Similar to a [bool], with an optional third indeterminate state and metadata.
4+
abstract class Fact<T> {
5+
const factory Fact.maybe([T? info]) = _Maybe;
6+
const Fact._();
7+
8+
T? get info => null;
9+
bool? get value => null;
10+
11+
bool get isDefinite => this is! _Maybe;
12+
13+
Fact<U> _asValue<U>() => value! ? true.asFact() : false.asFact();
14+
15+
Fact<T> get not {
16+
if (isDefinite) {
17+
return value! ? false.asFact() : true.asFact();
18+
}
19+
20+
return this;
21+
}
22+
23+
Fact<U> or<U>(Fact<U> other) => isDefinite ? _asValue() : other;
24+
25+
Fact<U> orElse<U>(Fact<U> Function() other) =>
26+
isDefinite ? _asValue() : other();
27+
28+
@override
29+
String toString() => isDefinite ? '(definite: $value)' : '(maybe: $info)';
30+
}
31+
32+
class _Bool<T> extends Fact<T> {
33+
@override
34+
final bool value;
35+
36+
// ignore: avoid_positional_boolean_parameters
37+
const _Bool(this.value) : super._();
38+
}
39+
40+
class _Maybe<T> extends Fact<T> {
41+
@override
42+
final T? info;
43+
44+
const _Maybe([this.info]) : super._();
45+
}
46+
47+
extension _BoolExt on bool {
48+
Fact<T> asFact<T>() => this ? const _Bool(true) : const _Bool(false);
49+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// ignore_for_file: parameter_assignments
2+
3+
part of 'use_setstate_synchronously_rule.dart';
4+
5+
typedef MountedFact = Fact<BinaryExpression>;
6+
7+
extension on BinaryExpression {
8+
bool get isOr => operator.type == TokenType.BAR_BAR;
9+
bool get isAnd => operator.type == TokenType.AMPERSAND_AMPERSAND;
10+
}
11+
12+
MountedFact _extractMountedCheck(
13+
Expression node, {
14+
bool permitAnd = true,
15+
bool expandOr = false,
16+
}) {
17+
// ![this.]mounted
18+
if (node is PrefixExpression &&
19+
node.operator.type == TokenType.BANG &&
20+
_isIdentifier(_thisOr(node.operand), 'mounted')) {
21+
return false.asFact();
22+
}
23+
24+
// [this.]mounted
25+
if (_isIdentifier(_thisOr(node), 'mounted')) {
26+
return true.asFact();
27+
}
28+
29+
if (node is BinaryExpression) {
30+
final right = node.rightOperand;
31+
// mounted && ..
32+
if (node.isAnd && permitAnd) {
33+
return _extractMountedCheck(node.leftOperand)
34+
.orElse(() => _extractMountedCheck(right));
35+
}
36+
37+
if (node.isOr) {
38+
if (!expandOr) {
39+
// Or-chains don't indicate anything in the then-branch yet,
40+
// but may yield information for the else-branch or divergence analysis.
41+
return Fact.maybe(node);
42+
}
43+
44+
return _extractMountedCheck(
45+
node.leftOperand,
46+
expandOr: expandOr,
47+
permitAnd: permitAnd,
48+
).orElse(() => _extractMountedCheck(
49+
right,
50+
expandOr: expandOr,
51+
permitAnd: permitAnd,
52+
));
53+
}
54+
}
55+
56+
return const Fact.maybe();
57+
}
58+
59+
/// If [fact] is indeterminate, try to recover a fact from its metadata.
60+
MountedFact _tryInvert(MountedFact fact) {
61+
final node = fact.info;
62+
63+
// a || b
64+
if (node != null && node.isOr) {
65+
return _extractMountedCheck(
66+
node.leftOperand,
67+
expandOr: true,
68+
permitAnd: false,
69+
)
70+
.orElse(
71+
() => _extractMountedCheck(
72+
node.rightOperand,
73+
expandOr: true,
74+
permitAnd: false,
75+
),
76+
)
77+
.not;
78+
}
79+
80+
return fact.not;
81+
}
82+
83+
@pragma('vm:prefer-inline')
84+
bool _isIdentifier(Expression node, String ident) =>
85+
node is Identifier && node.name == ident;
86+
87+
@pragma('vm:prefer-inline')
88+
bool _isDivergent(Statement node) =>
89+
node is ReturnStatement ||
90+
node is ExpressionStatement && node.expression is ThrowExpression;
91+
92+
@pragma('vm:prefer-inline')
93+
Expression _thisOr(Expression node) =>
94+
node is PropertyAccess && node.target is ThisExpression
95+
? node.propertyName
96+
: node;
97+
98+
bool _blockDiverges(Statement block) =>
99+
block is Block ? block.statements.any(_isDivergent) : _isDivergent(block);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/token.dart';
3+
import 'package:analyzer/dart/ast/visitor.dart';
4+
5+
import '../../../../../utils/flutter_types_utils.dart';
6+
import '../../../../../utils/node_utils.dart';
7+
import '../../../lint_utils.dart';
8+
import '../../../models/internal_resolved_unit_result.dart';
9+
import '../../../models/issue.dart';
10+
import '../../../models/severity.dart';
11+
import '../../models/flutter_rule.dart';
12+
import '../../rule_utils.dart';
13+
14+
part 'fact.dart';
15+
part 'helpers.dart';
16+
part 'config.dart';
17+
part 'visitor.dart';
18+
19+
class UseSetStateSynchronouslyRule extends FlutterRule {
20+
static const ruleId = 'use-setstate-synchronously';
21+
22+
Set<String> methods;
23+
24+
UseSetStateSynchronouslyRule([Map<String, Object> options = const {}])
25+
: methods = readMethods(options),
26+
super(
27+
id: ruleId,
28+
severity: readSeverity(options, Severity.warning),
29+
excludes: readExcludes(options),
30+
includes: readIncludes(options),
31+
);
32+
33+
@override
34+
Iterable<Issue> check(InternalResolvedUnitResult source) {
35+
final visitor = _Visitor(methods: methods);
36+
source.unit.visitChildren(visitor);
37+
38+
return visitor.nodes
39+
.map((node) => createIssue(
40+
rule: this,
41+
location: nodeLocation(node: node, source: source),
42+
message: "Avoid calling '${node.name}' past an await point "
43+
'without checking if the widget is mounted.',
44+
))
45+
.toList(growable: false);
46+
}
47+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
part of 'use_setstate_synchronously_rule.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final Set<String> methods;
5+
_Visitor({required this.methods});
6+
7+
final nodes = <SimpleIdentifier>[];
8+
9+
@override
10+
void visitClassDeclaration(ClassDeclaration node) {
11+
if (isWidgetStateOrSubclass(node.extendsClause?.superclass.type)) {
12+
node.visitChildren(this);
13+
}
14+
}
15+
16+
@override
17+
void visitBlockFunctionBody(BlockFunctionBody node) {
18+
if (!node.isAsynchronous) {
19+
return node.visitChildren(this);
20+
}
21+
final visitor = _AsyncSetStateVisitor(validateMethod: methods.contains);
22+
node.visitChildren(visitor);
23+
nodes.addAll(visitor.nodes);
24+
}
25+
}
26+
27+
class _AsyncSetStateVisitor extends RecursiveAstVisitor<void> {
28+
static bool _noop(String _) => false;
29+
30+
bool Function(String) validateMethod;
31+
_AsyncSetStateVisitor({this.validateMethod = _noop});
32+
33+
MountedFact mounted = true.asFact();
34+
bool inAsync = true;
35+
final nodes = <SimpleIdentifier>[];
36+
37+
@override
38+
void visitAwaitExpression(AwaitExpression node) {
39+
mounted = false.asFact();
40+
super.visitAwaitExpression(node);
41+
}
42+
43+
@override
44+
void visitMethodInvocation(MethodInvocation node) {
45+
if (!inAsync) {
46+
return node.visitChildren(this);
47+
}
48+
49+
// [this.]setState()
50+
final mounted_ = mounted.value ?? false;
51+
if (!mounted_ &&
52+
validateMethod(node.methodName.name) &&
53+
node.target is ThisExpression?) {
54+
nodes.add(node.methodName);
55+
}
56+
57+
super.visitMethodInvocation(node);
58+
}
59+
60+
@override
61+
void visitIfStatement(IfStatement node) {
62+
if (!inAsync) {
63+
return node.visitChildren(this);
64+
}
65+
66+
node.condition.visitChildren(this);
67+
final oldMounted = mounted;
68+
final newMounted = _extractMountedCheck(node.condition);
69+
70+
mounted = newMounted.or(mounted);
71+
final beforeThen = mounted;
72+
node.thenStatement.visitChildren(this);
73+
final afterThen = mounted;
74+
75+
var elseDiverges = false;
76+
final elseStatement = node.elseStatement;
77+
if (elseStatement != null) {
78+
elseDiverges = _blockDiverges(elseStatement);
79+
mounted = _tryInvert(newMounted).or(mounted);
80+
elseStatement.visitChildren(this);
81+
}
82+
83+
if (_blockDiverges(node.thenStatement)) {
84+
mounted = _tryInvert(newMounted).or(beforeThen);
85+
} else if (elseDiverges) {
86+
mounted = beforeThen != afterThen
87+
? afterThen
88+
: _extractMountedCheck(node.condition, permitAnd: false);
89+
} else {
90+
mounted = oldMounted;
91+
}
92+
}
93+
94+
@override
95+
void visitWhileStatement(WhileStatement node) {
96+
if (!inAsync) {
97+
return node.visitChildren(this);
98+
}
99+
100+
node.condition.visitChildren(this);
101+
final oldMounted = mounted;
102+
final newMounted = _extractMountedCheck(node.condition);
103+
mounted = newMounted.or(mounted);
104+
node.body.visitChildren(this);
105+
106+
mounted = _blockDiverges(node.body) ? _tryInvert(newMounted) : oldMounted;
107+
}
108+
109+
@override
110+
void visitBlockFunctionBody(BlockFunctionBody node) {
111+
final oldMounted = mounted;
112+
final oldInAsync = inAsync;
113+
mounted = true.asFact();
114+
inAsync = node.isAsynchronous;
115+
116+
node.visitChildren(this);
117+
118+
mounted = oldMounted;
119+
inAsync = oldInAsync;
120+
}
121+
}

0 commit comments

Comments
 (0)