diff --git a/packages/leancode_lint/.fvmrc b/packages/leancode_lint/.fvmrc index d80cf577..457360f8 100644 --- a/packages/leancode_lint/.fvmrc +++ b/packages/leancode_lint/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.41.9" + "flutter": "3.44.0" } \ No newline at end of file diff --git a/packages/leancode_lint/CHANGELOG.md b/packages/leancode_lint/CHANGELOG.md index 7ef3966d..66f8f0e3 100644 --- a/packages/leancode_lint/CHANGELOG.md +++ b/packages/leancode_lint/CHANGELOG.md @@ -1,6 +1,7 @@ # 24.0.0 -- Add new custom lint: +- Add new custom lints: + - [`avoid_catch_error`](https://github.com/leancodepl/flutter_corelibrary/tree/master/packages/leancode_lint#avoid_catch_error) - [`missing_equatable_props`](https://github.com/leancodepl/flutter_corelibrary/tree/master/packages/leancode_lint#missing_equatable_props) # 23.0.0 diff --git a/packages/leancode_lint/README.md b/packages/leancode_lint/README.md index 10e6ca4c..38d4d644 100644 --- a/packages/leancode_lint/README.md +++ b/packages/leancode_lint/README.md @@ -140,6 +140,44 @@ None. +
+avoid_catch_error + +### `avoid_catch_error` + +**AVOID** using `Future.catchError`. + +Prefer `async`/`await` with `try`/`catch`, which keeps error handling in normal +control flow and preserves stack traces more predictably. + +**BAD:** + +```dart +Future load() { + return repository.load().catchError((err, st) { + logger.error(err, st); + }); +} +``` + +**GOOD:** + +```dart +Future load() async { + try { + await repository.load(); + } catch (err, st) { + logger.error(err, st); + } +} +``` + +#### Configuration + +None. + +
+
avoid_conditional_hooks diff --git a/packages/leancode_lint/lib/plugin.dart b/packages/leancode_lint/lib/plugin.dart index 8c800a74..6478b277 100644 --- a/packages/leancode_lint/lib/plugin.dart +++ b/packages/leancode_lint/lib/plugin.dart @@ -5,6 +5,7 @@ import 'package:leancode_lint/src/assists/convert_iterable_map_to_collection_for import 'package:leancode_lint/src/assists/convert_positional_to_named_formal.dart'; import 'package:leancode_lint/src/assists/convert_record_into_nominal_type.dart'; import 'package:leancode_lint/src/lints/add_cubit_suffix_for_cubits.dart'; +import 'package:leancode_lint/src/lints/avoid_catch_error.dart'; import 'package:leancode_lint/src/lints/avoid_conditional_hooks.dart'; import 'package:leancode_lint/src/lints/avoid_single_child_in_multi_child_widget.dart'; import 'package:leancode_lint/src/lints/bloc_related_class_naming.dart'; @@ -60,6 +61,7 @@ final class LeanCodeLintPlugin extends Plugin { ..registerWarningRule( CatchParameterNames(config: config.catchParameterNames), ) + ..registerWarningRule(AvoidCatchError()) ..registerWarningRule(AvoidConditionalHooks()) ..registerWarningRule(HookWidgetDoesNotUseHooks()) ..registerFixForRule( diff --git a/packages/leancode_lint/lib/src/lints/avoid_catch_error.dart b/packages/leancode_lint/lib/src/lints/avoid_catch_error.dart new file mode 100644 index 00000000..f5fb03a6 --- /dev/null +++ b/packages/leancode_lint/lib/src/lints/avoid_catch_error.dart @@ -0,0 +1,54 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart'; + +class AvoidCatchError extends AnalysisRule { + AvoidCatchError() + : super(name: code.lowerCaseName, description: code.problemMessage); + + static const code = LintCode( + 'avoid_catch_error', + 'Avoid using Future.catchError.', + correctionMessage: 'Use async/await with try/catch instead.', + severity: .WARNING, + ); + + @override + LintCode get diagnosticCode => code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + registry.addMethodInvocation(this, _Visitor(this, context)); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.methodName case SimpleIdentifier( + name: 'catchError', + element: Element( + baseElement: MethodElement( + enclosingElement: InterfaceElement( + thisType: InterfaceType(isDartAsyncFuture: true), + ), + ), + ), + )) { + rule.reportAtNode(node.methodName); + } + } +} diff --git a/packages/leancode_lint/test/test_cases/avoid_catch_error_test.dart b/packages/leancode_lint/test/test_cases/avoid_catch_error_test.dart new file mode 100644 index 00000000..22ff8a3d --- /dev/null +++ b/packages/leancode_lint/test/test_cases/avoid_catch_error_test.dart @@ -0,0 +1,95 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:leancode_lint/src/lints/avoid_catch_error.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../assert_ranges.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidCatchErrorTest); + }); +} + +@reflectiveTest +class AvoidCatchErrorTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidCatchError(); + + super.setUp(); + } + + Future test_future_catch_error_is_marked() async { + await assertDiagnosticsInRanges(''' +import 'dart:async'; + +Future doSomething() async {} + +Future test(Future future) { + return future.[!catchError!]((Object err, StackTrace st) { + return doSomething(); + }); +} +'''); + } + + Future test_future_catch_error_cascade_is_marked() async { + await assertDiagnosticsInRanges(''' +import 'dart:async'; + +Future test(Future future) async { + future..[!catchError!]((Object err, StackTrace st) {}); +} +'''); + } + + Future test_catch_error_does_not_catch_synchronous_throw() async { + await assertDiagnosticsInRanges(r''' +import 'dart:async'; + +Future syncThrowing() { + throw 'sync'; +} + +Future asyncThrowing() async { + throw 'async'; +} + +Future main() async { + try { + await asyncThrowing()./*[0*/catchError/*0]*/((e) => print('Caught $e')); + await syncThrowing()./*[1*/catchError/*1]*/((e) => print('Caught $e')); + } catch (e) { + print('Uncaught $e'); + } +} +'''); + } + + Future test_try_catch_is_not_marked() async { + await assertNoDiagnostics(''' +import 'dart:async'; + +Future test(Future future) async { + try { + await future; + } catch (err, st) { + print(st); + throw err; + } +} +'''); + } + + Future test_custom_method_named_catch_error_is_not_marked() async { + await assertNoDiagnostics(''' +class HasCatchError { + void catchError(void Function() onError) {} +} + +void test(HasCatchError value) { + value.catchError(() {}); +} +'''); + } +}