diff --git a/packages/riverpod_lint/README.md b/packages/riverpod_lint/README.md index a4a073e88..175ea7fec 100644 --- a/packages/riverpod_lint/README.md +++ b/packages/riverpod_lint/README.md @@ -49,6 +49,7 @@ Riverpod_lint adds various warnings with quick fixes and refactoring options, su - [unsupported\_provider\_value (riverpod\_generator only)](#unsupported_provider_value-riverpod_generator-only) - [stateless\_ref (riverpod\_generator only)](#stateless_ref-riverpod_generator-only) - [generator\_class\_extends (riverpod\_generator only)](#generator_class_extends-riverpod_generator-only) + - [avoid\_ref\_inside\_state\_dispose](#avoid_ref_inside_state_dispose) - [All assists](#all-assists) - [Wrap widgets with a `Consumer`](#wrap-widgets-with-a-consumer) - [Wrap widgets with a `ProviderScope`](#wrap-widgets-with-a-providerscope) @@ -531,6 +532,25 @@ class Example extends Anything { } ``` +### avoid_ref_inside_state_dispose + +Avoid using `Ref` in the dispose method. + +**Bad**: + +```dart +class _MyWidgetState extends ConsumerState { + @override + void dispose() { + // Do not use 'ref' in the dispose method + ref.read(provider).doSomething(); + super.dispose(); + } + + // ... +} +``` + ## All assists ### Wrap widgets with a `Consumer` diff --git a/packages/riverpod_lint/lib/riverpod_lint.dart b/packages/riverpod_lint/lib/riverpod_lint.dart index 88a9737a0..5cbde7a44 100644 --- a/packages/riverpod_lint/lib/riverpod_lint.dart +++ b/packages/riverpod_lint/lib/riverpod_lint.dart @@ -9,6 +9,7 @@ import 'src/assists/wrap_with_consumer.dart'; import 'src/assists/wrap_with_provider_scope.dart'; import 'src/lints/avoid_manual_providers_as_generated_provider_dependency.dart'; import 'src/lints/avoid_public_notifier_properties.dart'; +import 'src/lints/avoid_ref_inside_state_dispose.dart'; import 'src/lints/generator_class_extends.dart'; import 'src/lints/missing_provider_scope.dart'; import 'src/lints/provider_dependencies.dart'; @@ -31,6 +32,7 @@ class _RiverpodPlugin extends PluginBase { const AvoidManualProvidersAsGeneratedProviderDependency(), const ScopedProvidersShouldSpecifyDependencies(), const UnsupportedProviderValue(), + const AvoidRefInsideStateDispose(), // const AvoidDynamicProviders(), // // "Avoid passing providers as parameter to objects" // const AvoidExposingProviderRef(), diff --git a/packages/riverpod_lint/lib/src/lints/avoid_ref_inside_state_dispose.dart b/packages/riverpod_lint/lib/src/lints/avoid_ref_inside_state_dispose.dart new file mode 100644 index 000000000..6b80eeaf4 --- /dev/null +++ b/packages/riverpod_lint/lib/src/lints/avoid_ref_inside_state_dispose.dart @@ -0,0 +1,58 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart'; + +import '../riverpod_custom_lint.dart'; + +const disposeMethod = 'dispose'; + +class AvoidRefInsideStateDispose extends RiverpodLintRule { + const AvoidRefInsideStateDispose() : super(code: _code); + + static const _code = LintCode( + name: 'avoid_ref_inside_state_dispose', + problemMessage: "Avoid using 'Ref' inside State.dispose.", + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addMethodInvocation((node) { + final targetType = node.realTarget?.staticType; + if (targetType == null) return; + if (!widgetRefType.isAssignableFromType(targetType)) return; + + final ancestor = node.thisOrAncestorMatching((method) { + final isDisposeMethod = + method is MethodDeclaration && method.name.lexeme == disposeMethod; + if (!isDisposeMethod) return false; + + return _findConsumerStateClass(node) != null; + }); + + if (ancestor != null) { + reporter.reportErrorForNode(_code, node); + } + }); + } + + /// Looking for the ConsumerState class ancestor + /// into the [node] parent. + AstNode? _findConsumerStateClass(AstNode node) { + return node.parent?.thisOrAncestorMatching((node) { + if (node is! ClassDeclaration) return false; + + /// Looking for the class which is a [ConsumerState] + final extendsClause = node.extendsClause; + if (extendsClause == null) return false; + final extendsType = extendsClause.superclass.type; + if (extendsType == null) return false; + + return consumerStateType.isExactlyType(extendsType); + }); + } +} diff --git a/packages/riverpod_lint_flutter_test/test/goldens/lints/avoid_ref_inside_state_dispose.dart b/packages/riverpod_lint_flutter_test/test/goldens/lints/avoid_ref_inside_state_dispose.dart new file mode 100644 index 000000000..3fc91393e --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/goldens/lints/avoid_ref_inside_state_dispose.dart @@ -0,0 +1,42 @@ +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final provider = Provider((ref) => 0); + +class MyWidget extends ConsumerStatefulWidget { + const MyWidget({super.key}); + + @override + ConsumerState createState() => _MyWidgetState(); +} + +class _MyWidgetState extends ConsumerState { + @override + void dispose() { + // expect_lint: avoid_ref_inside_state_dispose + ref.read(provider); + // expect_lint: avoid_ref_inside_state_dispose + ref.watch(provider); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +class PlainClass with ChangeNotifier { + PlainClass(this.ref); + + final WidgetRef ref; + + @override + void dispose() { + ref.read(provider); + ref.watch(provider); + + super.dispose(); + } +}