diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ae9035b0..29b641bdfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * feat: add static code diagnostic [`correct-game-instantiating`](https://dcm.dev/docs/individuals/rules/flame/correct-game-instantiating). * feat: add static code diagnostic [`avoid-initializing-in-on-mount`](https://dcm.dev/docs/individuals/rules/flame/avoid-initializing-in-on-mount). * feat: add static code diagnostic [`avoid-creating-vector-in-update`](https://dcm.dev/docs/individuals/rules/flame/avoid-creating-vector-in-update). +* feat: add static code diagnostic [`avoid-redundant-async-on-load`](https://dcm.dev/docs/individuals/rules/flame/avoid-redundant-async-on-load). ## 5.5.1 diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart index 8569d057d7..214862b889 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart @@ -21,6 +21,7 @@ import 'rules_list/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'rules_list/avoid_passing_async_when_sync_expected/avoid_passing_async_when_sync_expected_rule.dart'; import 'rules_list/avoid_preserve_whitespace_false/avoid_preserve_whitespace_false_rule.dart'; import 'rules_list/avoid_redundant_async/avoid_redundant_async_rule.dart'; +import 'rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule.dart'; import 'rules_list/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; import 'rules_list/avoid_shrink_wrap_in_lists/avoid_shrink_wrap_in_lists_rule.dart'; import 'rules_list/avoid_throw_in_catch_block/avoid_throw_in_catch_block_rule.dart'; @@ -103,6 +104,7 @@ final _implementedRules = )>{ AvoidPassingAsyncWhenSyncExpectedRule.new, AvoidPreserveWhitespaceFalseRule.ruleId: AvoidPreserveWhitespaceFalseRule.new, AvoidRedundantAsyncRule.ruleId: AvoidRedundantAsyncRule.new, + AvoidRedundantAsyncOnLoadRule.ruleId: AvoidRedundantAsyncOnLoadRule.new, AvoidReturningWidgetsRule.ruleId: AvoidReturningWidgetsRule.new, AvoidShrinkWrapInListsRule.ruleId: AvoidShrinkWrapInListsRule.new, AvoidThrowInCatchBlockRule.ruleId: AvoidThrowInCatchBlockRule.new, diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule.dart index 8a9a6fee1f..c2eb4a8452 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule.dart @@ -38,10 +38,7 @@ class AvoidRedundantAsyncRule extends CommonRule { return visitor.nodes.map( (node) => createIssue( rule: this, - location: nodeLocation( - node: node, - source: source, - ), + location: nodeLocation(node: node, source: source), message: _warningMessage, ), ); diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule.dart new file mode 100644 index 0000000000..327f6a0456 --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule.dart @@ -0,0 +1,46 @@ +// ignore_for_file: public_member_api_docs + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; + +import '../../../../../utils/flame_type_utils.dart'; +import '../../../../../utils/node_utils.dart'; +import '../../../lint_utils.dart'; +import '../../../models/internal_resolved_unit_result.dart'; +import '../../../models/issue.dart'; +import '../../../models/severity.dart'; +import '../../models/flame_rule.dart'; +import '../../rule_utils.dart'; + +part 'visitor.dart'; + +class AvoidRedundantAsyncOnLoadRule extends FlameRule { + static const String ruleId = 'avoid-redundant-async-on-load'; + + static const _warningMessage = "Avoid unnecessary async 'onLoad'."; + + AvoidRedundantAsyncOnLoadRule([Map config = const {}]) + : super( + id: ruleId, + severity: readSeverity(config, Severity.warning), + excludes: readExcludes(config), + includes: readIncludes(config), + ); + + @override + Iterable check(InternalResolvedUnitResult source) { + final visitor = _Visitor(); + + source.unit.visitChildren(visitor); + + return visitor.declarations + .map((declaration) => createIssue( + rule: this, + location: nodeLocation(node: declaration, source: source), + message: _warningMessage, + )) + .toList(growable: false); + } +} diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/visitor.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/visitor.dart new file mode 100644 index 0000000000..15ed1a3776 --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/visitor.dart @@ -0,0 +1,70 @@ +part of 'avoid_redundant_async_on_load_rule.dart'; + +class _Visitor extends SimpleAstVisitor { + final _declarations = []; + + Iterable get declarations => _declarations; + + @override + void visitClassDeclaration(ClassDeclaration node) { + super.visitClassDeclaration(node); + + final type = node.extendsClause?.superclass.type; + if (type == null || !isComponentOrSubclass(type)) { + return; + } + + final onLoadMethod = node.members.firstWhereOrNull((member) => + member is MethodDeclaration && + member.name.lexeme == 'onLoad' && + isOverride(member.metadata)); + + if (onLoadMethod is MethodDeclaration) { + if (_hasFutureType(onLoadMethod.returnType?.type) && + _hasRedundantAsync(onLoadMethod.body)) { + _declarations.add(onLoadMethod); + } + } + } + + bool _hasFutureType(DartType? type) => + type != null && (type.isDartAsyncFuture || type.isDartAsyncFutureOr); + + bool _hasRedundantAsync(FunctionBody body) { + if (body is ExpressionFunctionBody) { + final type = body.expression.staticType; + + if (type != null && + (type.isDartAsyncFuture || type.isDartAsyncFutureOr)) { + return false; + } + } + + final asyncVisitor = _AsyncVisitor(body); + body.parent?.visitChildren(asyncVisitor); + + return !asyncVisitor.hasValidAsync; + } +} + +class _AsyncVisitor extends RecursiveAstVisitor { + final FunctionBody body; + + bool hasValidAsync = false; + + _AsyncVisitor(this.body); + + @override + void visitReturnStatement(ReturnStatement node) { + super.visitReturnStatement(node); + + hasValidAsync = true; + } + + @override + void visitAwaitExpression(AwaitExpression node) { + super.visitAwaitExpression(node); + + hasValidAsync = true; + } +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule_test.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule_test.dart index 0ccf557ae1..5700757dab 100644 --- a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule_test.dart +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async/avoid_redundant_async_rule_test.dart @@ -19,7 +19,7 @@ void main() { ); }); - test('reports about found issues with the default config', () async { + test('reports about found issues', () async { final unit = await RuleTestHelper.resolveFromFile(_examplePath); final issues = AvoidRedundantAsyncRule().check(unit); diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule_test.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule_test.dart new file mode 100644 index 0000000000..e9c66017fd --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule_test.dart @@ -0,0 +1,49 @@ +import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart'; +import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/avoid_redundant_async_on_load_rule.dart'; +import 'package:test/test.dart'; + +import '../../../../../helpers/rule_test_helper.dart'; + +const _examplePath = 'avoid_redundant_async_on_load/examples/example.dart'; + +void main() { + group('AvoidRedundantAsyncOnLoadRule', () { + test('initialization', () async { + final unit = await RuleTestHelper.resolveFromFile(_examplePath); + final issues = AvoidRedundantAsyncOnLoadRule().check(unit); + + RuleTestHelper.verifyInitialization( + issues: issues, + ruleId: 'avoid-redundant-async-on-load', + severity: Severity.warning, + ); + }); + + test('reports about found issues', () async { + final unit = await RuleTestHelper.resolveFromFile(_examplePath); + final issues = AvoidRedundantAsyncOnLoadRule().check(unit); + + RuleTestHelper.verifyIssues( + issues: issues, + startLines: [6, 14, 22], + startColumns: [3, 3, 3], + locationTexts: [ + 'Future onLoad() {\n' + " print('hello');\n" + ' }', + 'FutureOr onLoad() {\n' + " print('hello');\n" + ' }', + 'FutureOr onLoad() async {\n' + " print('hello');\n" + ' }', + ], + messages: [ + "Avoid unnecessary async 'onLoad'.", + "Avoid unnecessary async 'onLoad'.", + "Avoid unnecessary async 'onLoad'.", + ], + ); + }); + }); +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/examples/example.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/examples/example.dart new file mode 100644 index 0000000000..f2ee125235 --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_redundant_async_on_load/examples/example.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +class MyComponent extends Component { + // LINT + @override + Future onLoad() { + print('hello'); + } +} + +class MyComponent extends Component { + // LINT + @override + FutureOr onLoad() { + print('hello'); + } +} + +class MyComponent extends Component { + // LINT + @override + FutureOr onLoad() async { + print('hello'); + } +} + +class MyComponent extends Component { + @override + Future onLoad() async { + await someAsyncMethod(); + } + + Future someAsyncMethod() => Future.value(null); +} + +class MyComponent extends Component { + @override + Future onLoad() async { + return someAsyncMethod(); + } + + Future someAsyncMethod() => Future.value(null); +} + +class MyComponent extends Component { + @override + Future onLoad() => someAsyncMethod(); + + Future someAsyncMethod() => Future.value(null); +} + +class MyComponent extends Component { + @override + void onLoad() { + print('hello'); + } +} + +class Component { + FutureOr onLoad() {} +}