diff --git a/docs/src/components/lint-rules/prefer_bloc_provider/BadSnippet.mdx b/docs/src/components/lint-rules/prefer_bloc_provider/BadSnippet.mdx new file mode 100644 index 00000000000..c0ea38f0624 --- /dev/null +++ b/docs/src/components/lint-rules/prefer_bloc_provider/BadSnippet.mdx @@ -0,0 +1,23 @@ +import { Code } from '@astrojs/starlight/components'; +import { transformerMetaHighlight } from '@shikijs/transformers'; + +( + create: (_) => CounterBloc(), + child: const CounterView(), + ); + } + } +`} +lang="dart" title="counter_page.dart" +transformers={[transformerMetaHighlight()]} class='warning' meta="{9}" +/> diff --git a/docs/src/components/lint-rules/prefer_bloc_provider/GoodSnippet.astro b/docs/src/components/lint-rules/prefer_bloc_provider/GoodSnippet.astro new file mode 100644 index 00000000000..1eee40a2f2e --- /dev/null +++ b/docs/src/components/lint-rules/prefer_bloc_provider/GoodSnippet.astro @@ -0,0 +1,22 @@ +--- +import { Code } from '@astrojs/starlight/components'; + +const code = ` +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CounterPage extends StatelessWidget { + const CounterPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CounterBloc(), + child: const CounterView(), + ); + } +} +`; +--- + + diff --git a/docs/src/content/docs/lint-rules/prefer_bloc_provider.mdx b/docs/src/content/docs/lint-rules/prefer_bloc_provider.mdx new file mode 100644 index 00000000000..955c64fbf49 --- /dev/null +++ b/docs/src/content/docs/lint-rules/prefer_bloc_provider.mdx @@ -0,0 +1,48 @@ +--- +title: Prefer Bloc Provider +description: The prefer_bloc_provider rule. +--- + +import { Badge } from '@astrojs/starlight/components'; +import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; +import BadSnippet from '~/components/lint-rules/prefer_bloc_provider/BadSnippet.mdx'; +import GoodSnippet from '~/components/lint-rules/prefer_bloc_provider/GoodSnippet.astro'; + +
+ + +
+ +Prefer using `BlocProvider` to provide `Bloc` or `Cubit` instances. + +:::note + +This lint rule was introduced in version `0.4.3` of +[`package:bloc_lint`](https://pub.dev/packages/bloc_lint) + +::: + +## Rationale + +`BlocProvider` is the intended API for providing `Bloc` and `Cubit` instances. +Unlike `Provider`, it automatically closes the instance when it is no longer +needed. + +## Examples + +**Avoid** using `Provider` when `T` is a `Bloc` or `Cubit`. + +**BAD**: + + + +**GOOD**: + + + +## Enable + +To enable the `prefer_bloc_provider` rule, add it to your +`analysis_options.yaml` under `bloc` > `rules`: + + diff --git a/packages/bloc_lint/CHANGELOG.md b/packages/bloc_lint/CHANGELOG.md index 5cef009f4c9..921d242291a 100644 --- a/packages/bloc_lint/CHANGELOG.md +++ b/packages/bloc_lint/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.4.3 + +- feat: add [prefer_bloc_provider](https://bloclibrary.dev/lint-rules/prefer_bloc_provider) + # 0.4.2 - deps: adjust bounds for `_fe_analyzer_shared` diff --git a/packages/bloc_lint/README.md b/packages/bloc_lint/README.md index 636093d53b4..b17cce00ccd 100644 --- a/packages/bloc_lint/README.md +++ b/packages/bloc_lint/README.md @@ -91,6 +91,7 @@ For more information, check out the [official documentation](https://bloclibrary - [avoid_flutter_imports](https://bloclibrary.dev/lint-rules/avoid_flutter_imports) - [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods) - [avoid_public_fields](https://bloclibrary.dev/lint-rules/avoid_public_fields) +- [prefer_bloc_provider](https://bloclibrary.dev/lint-rules/prefer_bloc_provider) - [prefer_file_naming_conventions](https://bloclibrary.dev/lint-rules/prefer_file_naming_conventions) - [prefer_void_public_cubit_methods](https://bloclibrary.dev/lint-rules/prefer_void_public_cubit_methods) @@ -101,6 +102,7 @@ For more information, check out the [official documentation](https://bloclibrary - [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods) - [avoid_public_fields](https://bloclibrary.dev/lint-rules/avoid_public_fields) - [prefer_bloc](https://bloclibrary.dev/lint-rules/prefer_bloc) +- [prefer_bloc_provider](https://bloclibrary.dev/lint-rules/prefer_bloc_provider) - [prefer_build_context_extensions](https://bloclibrary.dev/lint-rules/prefer_build_context_extensions) - [prefer_cubit](https://bloclibrary.dev/lint-rules/prefer_cubit) - [prefer_file_naming_conventions](https://bloclibrary.dev/lint-rules/prefer_file_naming_conventions) diff --git a/packages/bloc_lint/lib/all.yaml b/packages/bloc_lint/lib/all.yaml index d7720e9a240..4d50e2b4ee2 100644 --- a/packages/bloc_lint/lib/all.yaml +++ b/packages/bloc_lint/lib/all.yaml @@ -5,6 +5,7 @@ bloc: - avoid_public_bloc_methods - avoid_public_fields - prefer_bloc + - prefer_bloc_provider - prefer_build_context_extensions - prefer_cubit - prefer_file_naming_conventions diff --git a/packages/bloc_lint/lib/bloc_lint.dart b/packages/bloc_lint/lib/bloc_lint.dart index 9db741ce5be..682b69dd948 100644 --- a/packages/bloc_lint/lib/bloc_lint.dart +++ b/packages/bloc_lint/lib/bloc_lint.dart @@ -13,6 +13,7 @@ export 'src/rules/rules.dart' AvoidPublicBlocMethods, AvoidPublicFields, PreferBloc, + PreferBlocProvider, PreferBuildContextExtensions, PreferCubit, PreferFileNamingConventions, diff --git a/packages/bloc_lint/lib/recommended.yaml b/packages/bloc_lint/lib/recommended.yaml index 5e3b277bfdb..be1951f4632 100644 --- a/packages/bloc_lint/lib/recommended.yaml +++ b/packages/bloc_lint/lib/recommended.yaml @@ -3,5 +3,6 @@ bloc: - avoid_flutter_imports - avoid_public_bloc_methods - avoid_public_fields + - prefer_bloc_provider - prefer_file_naming_conventions - prefer_void_public_cubit_methods diff --git a/packages/bloc_lint/lib/src/linter.dart b/packages/bloc_lint/lib/src/linter.dart index db24ec826d4..c4767b0c16b 100644 --- a/packages/bloc_lint/lib/src/linter.dart +++ b/packages/bloc_lint/lib/src/linter.dart @@ -26,6 +26,7 @@ final allRules = { AvoidPublicBlocMethods.rule: AvoidPublicBlocMethods.new, AvoidPublicFields.rule: AvoidPublicFields.new, PreferBloc.rule: PreferBloc.new, + PreferBlocProvider.rule: PreferBlocProvider.new, PreferBuildContextExtensions.rule: PreferBuildContextExtensions.new, PreferCubit.rule: PreferCubit.new, PreferFileNamingConventions.rule: PreferFileNamingConventions.new, diff --git a/packages/bloc_lint/lib/src/rules/prefer_bloc_provider.dart b/packages/bloc_lint/lib/src/rules/prefer_bloc_provider.dart new file mode 100644 index 00000000000..60959e942ef --- /dev/null +++ b/packages/bloc_lint/lib/src/rules/prefer_bloc_provider.dart @@ -0,0 +1,59 @@ +import 'package:bloc_lint/bloc_lint.dart'; + +/// {@template prefer_bloc_provider} +/// The prefer_bloc_provider lint rule. +/// {@endtemplate} +class PreferBlocProvider extends LintRule { + /// {@macro prefer_bloc_provider} + PreferBlocProvider([Severity? severity]) + : super(name: rule, severity: severity ?? Severity.warning); + + /// The name of the lint rule. + static const rule = 'prefer_bloc_provider'; + + @override + Listener create(LintContext context) => _Listener(context); +} + +class _Listener extends Listener { + _Listener(this.context); + + final LintContext context; + + @override + void handleIdentifier(Token token, IdentifierContext _) { + if (token.lexeme != 'Provider') return; + + final typeArgument = _typeArgumentName(token); + if (typeArgument == null) return; + if (!_looksLikeBlocBase(typeArgument.lexeme)) return; + + context.reportToken( + token: token, + message: 'Avoid using Provider to provide bloc or cubit instances.', + hint: 'Prefer using BlocProvider instead.', + ); + } + + Token? _typeArgumentName(Token provider) { + final lt = provider.next; + if (lt == null || lt.type != TokenType.LT) return null; + + var type = lt.next; + if (type == null) return null; + + // Skip import prefix: counter.CounterBloc + if (type.next?.type == TokenType.PERIOD) { + type = type.next!.next; + if (type == null) return null; + } + + return type; + } + + bool _looksLikeBlocBase(String name) { + return name.endsWith('BlocBase') || + name.endsWith('Bloc') || + name.endsWith('Cubit'); + } +} diff --git a/packages/bloc_lint/lib/src/rules/rules.dart b/packages/bloc_lint/lib/src/rules/rules.dart index cf15d858171..340d9a21776 100644 --- a/packages/bloc_lint/lib/src/rules/rules.dart +++ b/packages/bloc_lint/lib/src/rules/rules.dart @@ -3,6 +3,7 @@ export 'avoid_flutter_imports.dart'; export 'avoid_public_bloc_methods.dart'; export 'avoid_public_fields.dart'; export 'prefer_bloc.dart'; +export 'prefer_bloc_provider.dart'; export 'prefer_build_context_extensions.dart'; export 'prefer_cubit.dart'; export 'prefer_file_naming_conventions.dart'; diff --git a/packages/bloc_lint/pubspec.yaml b/packages/bloc_lint/pubspec.yaml index 1233aa76e18..0af32cdfa54 100644 --- a/packages/bloc_lint/pubspec.yaml +++ b/packages/bloc_lint/pubspec.yaml @@ -1,6 +1,6 @@ name: bloc_lint description: Official lint rules for development when using the bloc state management library. -version: 0.4.2 +version: 0.4.3 repository: https://github.com/felangel/bloc/tree/master/packages/bloc_lint issue_tracker: https://github.com/felangel/bloc/issues homepage: https://github.com/felangel/bloc diff --git a/packages/bloc_lint/test/src/rules/prefer_bloc_provider_test.dart b/packages/bloc_lint/test/src/rules/prefer_bloc_provider_test.dart new file mode 100644 index 00000000000..c5f52de9dc3 --- /dev/null +++ b/packages/bloc_lint/test/src/rules/prefer_bloc_provider_test.dart @@ -0,0 +1,235 @@ +import 'package:bloc_lint/src/rules/rules.dart'; +import 'package:test/test.dart'; + +import '../lint_test_helper.dart'; + +void main() { + group(PreferBlocProvider, () { + lintTest( + 'lints when using Provider with a Bloc type', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Provider( + ^^^^^^^^ + create: (_) => CounterBloc(), + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'lints when using Provider with a Cubit type', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Provider( + ^^^^^^^^ + create: (_) => CounterCubit(), + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'lints when using Provider with BlocBase', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Provider( + ^^^^^^^^ + create: (_) => CounterCubit(), + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'lints when using Provider with a prefixed Bloc type', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:my_app/counter/counter.dart' as counter; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Provider( + ^^^^^^^^ + create: (_) => counter.CounterBloc(), + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'lints when using Provider with nested Bloc type arguments', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Provider>( + ^^^^^^^^ + create: (_) => CounterBloc(), + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'lints when using Provider.value with a Bloc type', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({required this.bloc, super.key}); + + final CounterBloc bloc; + + @override + Widget build(BuildContext context) { + return Provider.value( + ^^^^^^^^ + value: bloc, + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'does not lint when using BlocProvider', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CounterBloc(), + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'does not lint when using Provider with a non-bloc type', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Provider( + create: (_) => WeatherRepository(), + child: const SizedBox(), + ); + } +} +''', + ); + + lintTest( + 'does not lint when using Provider.of', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + final bloc = Provider.of(context); + return const SizedBox(); + } +} +''', + ); + + lintTest( + 'does not lint when using Provider without a type argument', + rule: PreferBlocProvider.new, + path: 'my_widget.dart', + content: ''' +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Provider( + create: (_) => WeatherRepository(), + child: const SizedBox(), + ); + } +} +''', + ); + }); +}