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

Commit c3a124a

Browse files
feat: add new rule correct-game-instantiating (#1163)
* feat: add new rule correct-game-instantiating * Update CHANGELOG.md --------- Co-authored-by: Dmitry Krutskikh <[email protected]>
1 parent 17c31b4 commit c3a124a

File tree

10 files changed

+284
-4
lines changed

10 files changed

+284
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
* docs: remove old website
6+
* feat: add static code diagnostic [`correct-game-instantiating`](https://dcm.dev/docs/individuals/rules/flame/correct-game-instantiating).
67

78
## 5.5.1
89

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'rule.dart';
2+
import 'rule_type.dart';
3+
4+
/// Represents a base class for Flutter-specific rules.
5+
abstract class FlameRule extends Rule {
6+
static const link = 'https://pub.dev/packages/flame';
7+
8+
const FlameRule({
9+
required super.id,
10+
required super.severity,
11+
required super.excludes,
12+
required super.includes,
13+
}) : super(
14+
type: RuleType.flame,
15+
);
16+
}

lib/src/analyzers/lint_analyzer/rules/models/rule_type.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ class RuleType {
88
static const flutter = RuleType._('flutter');
99
static const intl = RuleType._('intl');
1010
static const angular = RuleType._('angular');
11+
static const flame = RuleType._('flame');
1112
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import 'rules_list/binary_expression_operand_order/binary_expression_operand_ord
3535
import 'rules_list/check_for_equals_in_render_object_setters/check_for_equals_in_render_object_setters_rule.dart';
3636
import 'rules_list/component_annotation_arguments_ordering/component_annotation_arguments_ordering_rule.dart';
3737
import 'rules_list/consistent_update_render_object/consistent_update_render_object_rule.dart';
38+
import 'rules_list/correct_game_instantiating/correct_game_instantiating_rule.dart';
3839
import 'rules_list/double_literal_format/double_literal_format_rule.dart';
3940
import 'rules_list/format_comment/format_comment_rule.dart';
4041
import 'rules_list/list_all_equatable_fields/list_all_equatable_fields_rule.dart';
@@ -119,6 +120,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
119120
ComponentAnnotationArgumentsOrderingRule.ruleId:
120121
ComponentAnnotationArgumentsOrderingRule.new,
121122
ConsistentUpdateRenderObjectRule.ruleId: ConsistentUpdateRenderObjectRule.new,
123+
CorrectGameInstantiatingRule.ruleId: CorrectGameInstantiatingRule.new,
122124
DoubleLiteralFormatRule.ruleId: DoubleLiteralFormatRule.new,
123125
FormatCommentRule.ruleId: FormatCommentRule.new,
124126
ListAllEquatableFieldsRule.ruleId: ListAllEquatableFieldsRule.new,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// ignore_for_file: public_member_api_docs
2+
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer/dart/ast/visitor.dart';
5+
import 'package:collection/collection.dart';
6+
7+
import '../../../../../utils/flutter_types_utils.dart';
8+
import '../../../../../utils/node_utils.dart';
9+
import '../../../lint_utils.dart';
10+
import '../../../models/internal_resolved_unit_result.dart';
11+
import '../../../models/issue.dart';
12+
import '../../../models/replacement.dart';
13+
import '../../../models/severity.dart';
14+
import '../../models/flame_rule.dart';
15+
import '../../rule_utils.dart';
16+
17+
part 'visitor.dart';
18+
19+
class CorrectGameInstantiatingRule extends FlameRule {
20+
static const String ruleId = 'correct-game-instantiating';
21+
22+
static const _warningMessage =
23+
'Incorrect game instantiation. The game will reset on each rebuild.';
24+
static const _correctionMessage = "Replace with 'controlled'.";
25+
26+
CorrectGameInstantiatingRule([Map<String, Object> config = const {}])
27+
: super(
28+
id: ruleId,
29+
severity: readSeverity(config, Severity.warning),
30+
excludes: readExcludes(config),
31+
includes: readIncludes(config),
32+
);
33+
34+
@override
35+
Iterable<Issue> check(InternalResolvedUnitResult source) {
36+
final visitor = _Visitor();
37+
38+
source.unit.visitChildren(visitor);
39+
40+
return visitor.info
41+
.map((info) => createIssue(
42+
rule: this,
43+
location: nodeLocation(node: info.node, source: source),
44+
message: _warningMessage,
45+
replacement: _createReplacement(info),
46+
))
47+
.toList(growable: false);
48+
}
49+
50+
Replacement? _createReplacement(_InstantiationInfo info) {
51+
if (info.isStateless) {
52+
final arguments = info.node.argumentList.arguments.map((arg) {
53+
if (arg is NamedExpression && arg.name.label.name == 'game') {
54+
final expression = arg.expression;
55+
if (expression is InstanceCreationExpression) {
56+
final name =
57+
expression.staticType?.getDisplayString(withNullability: false);
58+
if (name != null) {
59+
return 'gameFactory: $name.new,';
60+
}
61+
}
62+
}
63+
64+
return arg.toSource();
65+
});
66+
67+
return Replacement(
68+
replacement: 'GameWidget.controlled$arguments;',
69+
comment: _correctionMessage,
70+
);
71+
}
72+
73+
return null;
74+
}
75+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
part of 'correct_game_instantiating_rule.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final _info = <_InstantiationInfo>[];
5+
6+
Iterable<_InstantiationInfo> get info => _info;
7+
8+
@override
9+
void visitClassDeclaration(ClassDeclaration node) {
10+
super.visitClassDeclaration(node);
11+
12+
final type = node.extendsClause?.superclass.type;
13+
if (type == null) {
14+
return;
15+
}
16+
17+
final isWidget = isWidgetOrSubclass(type);
18+
final isWidgetState = isWidgetStateOrSubclass(type);
19+
if (!isWidget && !isWidgetState) {
20+
return;
21+
}
22+
23+
final declaration = node.members.firstWhereOrNull((declaration) =>
24+
declaration is MethodDeclaration && declaration.name.lexeme == 'build');
25+
26+
if (declaration is MethodDeclaration) {
27+
final visitor = _GameWidgetInstantiatingVisitor();
28+
declaration.visitChildren(visitor);
29+
30+
if (visitor.wrongInstantiation != null) {
31+
_info.add(_InstantiationInfo(
32+
visitor.wrongInstantiation!,
33+
isStateless: !isWidgetState,
34+
));
35+
}
36+
}
37+
}
38+
}
39+
40+
class _GameWidgetInstantiatingVisitor extends RecursiveAstVisitor<void> {
41+
InstanceCreationExpression? wrongInstantiation;
42+
43+
_GameWidgetInstantiatingVisitor();
44+
45+
@override
46+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
47+
super.visitInstanceCreationExpression(node);
48+
49+
if (isGameWidget(node.staticType) &&
50+
node.constructorName.name?.name != 'controlled') {
51+
final gameArgument = node.argumentList.arguments.firstWhereOrNull(
52+
(argument) =>
53+
argument is NamedExpression && argument.name.label.name == 'game',
54+
);
55+
56+
if (gameArgument is NamedExpression &&
57+
gameArgument.expression is InstanceCreationExpression) {
58+
wrongInstantiation = node;
59+
}
60+
}
61+
}
62+
}
63+
64+
class _InstantiationInfo {
65+
final bool isStateless;
66+
final InstanceCreationExpression node;
67+
68+
const _InstantiationInfo(this.node, {required this.isStateless});
69+
}

lib/src/utils/flutter_types_utils.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ bool isPaddingWidget(DartType? type) =>
5050
bool isBuildContext(DartType? type) =>
5151
type?.getDisplayString(withNullability: false) == 'BuildContext';
5252

53+
bool isGameWidget(DartType? type) =>
54+
type?.getDisplayString(withNullability: false) == 'GameWidget';
55+
5356
bool _isWidget(DartType? type) =>
5457
type?.getDisplayString(withNullability: false) == 'Widget';
5558

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
2+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/correct_game_instantiating/correct_game_instantiating_rule.dart';
3+
import 'package:test/test.dart';
4+
5+
import '../../../../../helpers/rule_test_helper.dart';
6+
7+
const _examplePath = 'correct_game_instantiating/examples/example.dart';
8+
9+
void main() {
10+
group('CorrectGameInstantiatingRule', () {
11+
test('initialization', () async {
12+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
13+
final issues = CorrectGameInstantiatingRule().check(unit);
14+
15+
RuleTestHelper.verifyInitialization(
16+
issues: issues,
17+
ruleId: 'correct-game-instantiating',
18+
severity: Severity.warning,
19+
);
20+
});
21+
22+
test('reports about found issues', () async {
23+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
24+
final issues = CorrectGameInstantiatingRule().check(unit);
25+
26+
RuleTestHelper.verifyIssues(
27+
issues: issues,
28+
startLines: [4, 25],
29+
startColumns: [12, 12],
30+
locationTexts: [
31+
'GameWidget(game: MyFlameGame())',
32+
'GameWidget(game: MyFlameGame())',
33+
],
34+
messages: [
35+
'Incorrect game instantiation. The game will reset on each rebuild.',
36+
'Incorrect game instantiation. The game will reset on each rebuild.',
37+
],
38+
replacements: [
39+
'GameWidget.controlled(gameFactory: MyFlameGame.new,);',
40+
null,
41+
],
42+
replacementComments: [
43+
"Replace with 'controlled'.",
44+
null,
45+
],
46+
);
47+
});
48+
});
49+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
class MyGamePage extends StatelessWidget {
2+
@override
3+
Widget build(BuildContext context) {
4+
return GameWidget(game: MyFlameGame()); // LINT
5+
}
6+
}
7+
8+
class MyGamePage extends StatefulWidget {
9+
@override
10+
State<MyGamePage> createState() => _MyGamePageState();
11+
}
12+
13+
class _MyGamePageState extends State<MyGamePage> {
14+
late final _game = MyFlameGame();
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return GameWidget(game: _game);
19+
}
20+
}
21+
22+
class _MyGamePageState extends State<MyGamePage> {
23+
@override
24+
Widget build(BuildContext context) {
25+
return GameWidget(game: MyFlameGame()); // LINT
26+
}
27+
}
28+
29+
class MyGamePage extends StatelessWidget {
30+
@override
31+
Widget build(BuildContext context) {
32+
return GameWidget.controlled(gameFactory: MyFlameGame.new);
33+
}
34+
}
35+
36+
class GameWidget extends Widget {
37+
final Widget game;
38+
39+
GameWidget({required this.game});
40+
41+
GameWidget.controlled({required GameFactory<Widget> gameFactory}) {
42+
this.game = gameFactory();
43+
}
44+
}
45+
46+
class MyFlameGame extends Widget {}
47+
48+
typedef GameFactory<T> = T Function();
49+
50+
class StatefulWidget extends Widget {}
51+
52+
class StatelessWidget extends Widget {}
53+
54+
class Widget {
55+
const Widget();
56+
}
57+
58+
class BuildContext {}
59+
60+
abstract class State<T> {
61+
void initState();
62+
63+
void setState(VoidCallback callback) => callback();
64+
}

test/src/helpers/rule_test_helper.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ class RuleTestHelper {
4646
Iterable<int>? startColumns,
4747
Iterable<String>? locationTexts,
4848
Iterable<String>? messages,
49-
Iterable<String>? replacements,
50-
Iterable<String>? replacementComments,
49+
Iterable<String?>? replacements,
50+
Iterable<String?>? replacementComments,
5151
}) {
5252
if (startLines != null) {
5353
expect(
@@ -83,15 +83,15 @@ class RuleTestHelper {
8383

8484
if (replacements != null) {
8585
expect(
86-
issues.map((issue) => issue.suggestion!.replacement),
86+
issues.map((issue) => issue.suggestion?.replacement),
8787
equals(replacements),
8888
reason: 'incorrect replacement',
8989
);
9090
}
9191

9292
if (replacementComments != null) {
9393
expect(
94-
issues.map((issue) => issue.suggestion!.comment),
94+
issues.map((issue) => issue.suggestion?.comment),
9595
equals(replacementComments),
9696
reason: 'incorrect replacement comment',
9797
);

0 commit comments

Comments
 (0)