diff --git a/packages/riverpod_lint/README.md b/packages/riverpod_lint/README.md index 011c4ace7..a224f2327 100644 --- a/packages/riverpod_lint/README.md +++ b/packages/riverpod_lint/README.md @@ -44,6 +44,7 @@ Riverpod_lint adds various warnings with quick fixes and refactoring options, su - [provider\_dependencies (riverpod\_generator only)](#provider_dependencies-riverpod_generator-only) - [scoped\_providers\_should\_specify\_dependencies (generator only)](#scoped_providers_should_specify_dependencies-generator-only) - [avoid\_manual\_providers\_as\_generated\_provider\_dependency](#avoid_manual_providers_as_generated_provider_dependency) + - [avoid\_build\_context\_in\_providers (riverpod\_generator only)](#avoid_build_context_in_providers-riverpod_generator-only) - [provider\_parameters](#provider_parameters) - [avoid\_public\_notifier\_properties](#avoid_public_notifier_properties) - [unsupported\_provider\_value (riverpod\_generator only)](#unsupported_provider_value-riverpod_generator-only) @@ -346,6 +347,39 @@ int example(ExampleRef ref) { } ``` +### avoid_build_context_in_providers (riverpod_generator only) + +Providers should not interact with `BuildContext`. + +**Good**: + +```dart +@riverpod +int fn(FnRef ref) => 0; + +@riverpod +class MyNotifier extends _$MyNotifier { + int build() => 0; + + void event() {} +} +``` + +**Bad**: + +```dart +// Providers should not receive a BuildContext as a parameter. +int fn(FnRef ref, BuildContext context) => 0; + +@riverpod +class MyNotifier extends _$MyNotifier { + int build() => 0; + + // Notifiers should not have methods that receive a BuildContext as a parameter. + void event(BuildContext context) {} +} +``` + ### provider_parameters Providers' parameters should have a consistent ==. Meaning either the values should be cached, or the parameters should override ==. diff --git a/packages/riverpod_lint/lib/riverpod_lint.dart b/packages/riverpod_lint/lib/riverpod_lint.dart index 41091e91b..b20d8ef47 100644 --- a/packages/riverpod_lint/lib/riverpod_lint.dart +++ b/packages/riverpod_lint/lib/riverpod_lint.dart @@ -7,6 +7,7 @@ import 'src/assists/convert_to_widget_utils.dart'; import 'src/assists/functional_to_class_based_provider.dart'; import 'src/assists/wrap_with_consumer.dart'; import 'src/assists/wrap_with_provider_scope.dart'; +import 'src/lints/avoid_build_context_in_providers.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'; @@ -24,6 +25,7 @@ PluginBase createPlugin() => _RiverpodPlugin(); class _RiverpodPlugin extends PluginBase { @override List getLintRules(CustomLintConfigs configs) => [ + const AvoidBuildContextInProviders(), const AvoidPublicNotifierProperties(), const FunctionalRef(), const MissingProviderScope(), diff --git a/packages/riverpod_lint/lib/src/lints/avoid_build_context_in_providers.dart b/packages/riverpod_lint/lib/src/lints/avoid_build_context_in_providers.dart new file mode 100644 index 000000000..bd29cf7a4 --- /dev/null +++ b/packages/riverpod_lint/lib/src/lints/avoid_build_context_in_providers.dart @@ -0,0 +1,58 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import '../riverpod_custom_lint.dart'; + +const TypeChecker buildContextType = TypeChecker.fromName( + 'BuildContext', + packageName: 'flutter', +); + +class AvoidBuildContextInProviders extends RiverpodLintRule { + const AvoidBuildContextInProviders() : super(code: _code); + + static const _code = LintCode( + name: 'avoid_build_context_in_providers', + problemMessage: + 'Passing BuildContext to providers indicates mixing UI with the business logic.', + errorSeverity: ErrorSeverity.WARNING, + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + riverpodRegistry(context).addFunctionalProviderDeclaration((declaration) { + final parameters = declaration.node.functionExpression.parameters!; + _emitWarningsForBuildContext(reporter, parameters); + }); + + riverpodRegistry(context).addClassBasedProviderDeclaration((declaration) { + final methods = declaration.node.members.whereType(); + + for (final method in methods) { + final parameters = method.parameters!; + _emitWarningsForBuildContext(reporter, parameters); + } + }); + } + + void _emitWarningsForBuildContext( + ErrorReporter reporter, + FormalParameterList parameters, + ) { + final buildContextParameters = parameters.parameters.where( + (e) => + e.declaredElement?.type != null && + buildContextType.isExactlyType(e.declaredElement!.type), + ); + + for (final contextParameter in buildContextParameters) { + reporter.reportErrorForNode(_code, contextParameter); + } + } +} diff --git a/packages/riverpod_lint_flutter_test/test/lints/avoid_build_context_in_providers.dart b/packages/riverpod_lint_flutter_test/test/lints/avoid_build_context_in_providers.dart new file mode 100644 index 000000000..0992777e1 --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/lints/avoid_build_context_in_providers.dart @@ -0,0 +1,32 @@ +import 'package:flutter/widgets.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'avoid_build_context_in_providers.g.dart'; + +@riverpod +int fn( + FnRef ref, + // expect_lint: avoid_build_context_in_providers + BuildContext context1, { + // expect_lint: avoid_build_context_in_providers + required BuildContext context2, +}) => + 0; + +@riverpod +class MyNotifier extends _$MyNotifier { + int build( + // expect_lint: avoid_build_context_in_providers + BuildContext context1, { + // expect_lint: avoid_build_context_in_providers + required BuildContext context2, + }) => + 0; + + void event( + // expect_lint: avoid_build_context_in_providers + BuildContext context3, { + // expect_lint: avoid_build_context_in_providers + required BuildContext context4, + }) {} +} diff --git a/packages/riverpod_lint_flutter_test/test/lints/avoid_build_context_in_providers.g.dart b/packages/riverpod_lint_flutter_test/test/lints/avoid_build_context_in_providers.g.dart new file mode 100644 index 000000000..3b4658b59 --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/lints/avoid_build_context_in_providers.g.dart @@ -0,0 +1,336 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'avoid_build_context_in_providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$fnHash() => r'7b8d0cf179067c80b8553b3232fd886fac83f387'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [fn]. +@ProviderFor(fn) +const fnProvider = FnFamily(); + +/// See also [fn]. +class FnFamily extends Family { + /// See also [fn]. + const FnFamily(); + + /// See also [fn]. + FnProvider call( + BuildContext context1, { + required BuildContext context2, + }) { + return FnProvider( + context1, + context2: context2, + ); + } + + @override + FnProvider getProviderOverride( + covariant FnProvider provider, + ) { + return call( + provider.context1, + context2: provider.context2, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'fnProvider'; +} + +/// See also [fn]. +class FnProvider extends AutoDisposeProvider { + /// See also [fn]. + FnProvider( + BuildContext context1, { + required BuildContext context2, + }) : this._internal( + (ref) => fn( + ref as FnRef, + context1, + context2: context2, + ), + from: fnProvider, + name: r'fnProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$fnHash, + dependencies: FnFamily._dependencies, + allTransitiveDependencies: FnFamily._allTransitiveDependencies, + context1: context1, + context2: context2, + ); + + FnProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.context1, + required this.context2, + }) : super.internal(); + + final BuildContext context1; + final BuildContext context2; + + @override + Override overrideWith( + int Function(FnRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: FnProvider._internal( + (ref) => create(ref as FnRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + context1: context1, + context2: context2, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _FnProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is FnProvider && + other.context1 == context1 && + other.context2 == context2; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, context1.hashCode); + hash = _SystemHash.combine(hash, context2.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin FnRef on AutoDisposeProviderRef { + /// The parameter `context1` of this provider. + BuildContext get context1; + + /// The parameter `context2` of this provider. + BuildContext get context2; +} + +class _FnProviderElement extends AutoDisposeProviderElement with FnRef { + _FnProviderElement(super.provider); + + @override + BuildContext get context1 => (origin as FnProvider).context1; + @override + BuildContext get context2 => (origin as FnProvider).context2; +} + +String _$myNotifierHash() => r'04a0cf33dbda80e3fa80748fe46546b1c968da22'; + +abstract class _$MyNotifier extends BuildlessAutoDisposeNotifier { + late final BuildContext context1; + late final BuildContext context2; + + int build( + BuildContext context1, { + required BuildContext context2, + }); +} + +/// See also [MyNotifier]. +@ProviderFor(MyNotifier) +const myNotifierProvider = MyNotifierFamily(); + +/// See also [MyNotifier]. +class MyNotifierFamily extends Family { + /// See also [MyNotifier]. + const MyNotifierFamily(); + + /// See also [MyNotifier]. + MyNotifierProvider call( + BuildContext context1, { + required BuildContext context2, + }) { + return MyNotifierProvider( + context1, + context2: context2, + ); + } + + @override + MyNotifierProvider getProviderOverride( + covariant MyNotifierProvider provider, + ) { + return call( + provider.context1, + context2: provider.context2, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'myNotifierProvider'; +} + +/// See also [MyNotifier]. +class MyNotifierProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [MyNotifier]. + MyNotifierProvider( + BuildContext context1, { + required BuildContext context2, + }) : this._internal( + () => MyNotifier() + ..context1 = context1 + ..context2 = context2, + from: myNotifierProvider, + name: r'myNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$myNotifierHash, + dependencies: MyNotifierFamily._dependencies, + allTransitiveDependencies: + MyNotifierFamily._allTransitiveDependencies, + context1: context1, + context2: context2, + ); + + MyNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.context1, + required this.context2, + }) : super.internal(); + + final BuildContext context1; + final BuildContext context2; + + @override + int runNotifierBuild( + covariant MyNotifier notifier, + ) { + return notifier.build( + context1, + context2: context2, + ); + } + + @override + Override overrideWith(MyNotifier Function() create) { + return ProviderOverride( + origin: this, + override: MyNotifierProvider._internal( + () => create() + ..context1 = context1 + ..context2 = context2, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + context1: context1, + context2: context2, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement createElement() { + return _MyNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is MyNotifierProvider && + other.context1 == context1 && + other.context2 == context2; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, context1.hashCode); + hash = _SystemHash.combine(hash, context2.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin MyNotifierRef on AutoDisposeNotifierProviderRef { + /// The parameter `context1` of this provider. + BuildContext get context1; + + /// The parameter `context2` of this provider. + BuildContext get context2; +} + +class _MyNotifierProviderElement + extends AutoDisposeNotifierProviderElement + with MyNotifierRef { + _MyNotifierProviderElement(super.provider); + + @override + BuildContext get context1 => (origin as MyNotifierProvider).context1; + @override + BuildContext get context2 => (origin as MyNotifierProvider).context2; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member