diff --git a/docs/src/components/lint-rules/avoid_bloc_to_bloc_members/BadSnippet.mdx b/docs/src/components/lint-rules/avoid_bloc_to_bloc_members/BadSnippet.mdx new file mode 100644 index 00000000000..9694a83c419 --- /dev/null +++ b/docs/src/components/lint-rules/avoid_bloc_to_bloc_members/BadSnippet.mdx @@ -0,0 +1,25 @@ +import { Code } from '@astrojs/starlight/components'; +import { transformerMetaHighlight } from '@shikijs/transformers'; + + { + // Avoid blocs as members + final NotificationBloc _notificationBloc; + + CounterBloc(this._notificationBloc) : super(0) { + on((event, emit) { + emit(state + 1); + _notificationBloc.add(NotificationEvent(message: 'Counter value incremented')); + }); + } + } +`} +lang="dart" title="counter_page.dart" +transformers={[transformerMetaHighlight()]} class='warning' meta="{8}" +/> diff --git a/docs/src/components/lint-rules/avoid_bloc_to_bloc_members/GoodSnippet.astro b/docs/src/components/lint-rules/avoid_bloc_to_bloc_members/GoodSnippet.astro new file mode 100644 index 00000000000..0cd24dc235f --- /dev/null +++ b/docs/src/components/lint-rules/avoid_bloc_to_bloc_members/GoodSnippet.astro @@ -0,0 +1,26 @@ +--- +import { Code } from '@astrojs/starlight/components'; + +const code = ` +import '../bloc/counter_bloc.dart'; +import '../bloc/notification_bloc.dart'; + +class MyNotifier extends StatelessWidget { + final Widget child; + const MyNotifier({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + // Handle at the application layer + return BlocListener( + listener: (context, state) { + context.read().add(NotificationEvent(message: 'Counter value incremented')); + }, + child: child, + ); + } +} +`; +--- + + diff --git a/docs/src/content/docs/lint-rules/avoid_bloc_to_bloc_members.mdx b/docs/src/content/docs/lint-rules/avoid_bloc_to_bloc_members.mdx new file mode 100644 index 00000000000..8212e5d136a --- /dev/null +++ b/docs/src/content/docs/lint-rules/avoid_bloc_to_bloc_members.mdx @@ -0,0 +1,44 @@ +--- +title: Avoid Bloc-to-Bloc Members +description: The avoid_bloc_to_bloc_members rule +--- + +import { Badge } from '@astrojs/starlight/components'; +import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; +import BadSnippet from '~/components/lint-rules/avoid_bloc_to_bloc_members/BadSnippet.mdx'; +import GoodSnippet from '~/components/lint-rules/avoid_bloc_to_bloc_members/GoodSnippet.astro'; + +
+ + +
+ +Avoid including `Bloc`s or `Cubit`s as members of other `Bloc`s or `Cubit`s. + +## Rationale + +While passing blocs to other blocs may be error free, it creates dependencies +between blocs. + +See also +[Bloc-to-Bloc Communication](https://bloclibrary.dev/architecture/#bloc-to-bloc-communication) + +## Examples + +**Avoid** passing a `Bloc` or `Cubit` to the constructor of another `Bloc` or +`Cubit`. + +**BAD**: + + + +**GOOD**: + + + +## Enable + +To enable the `avoid_bloc_to_bloc_members` rule, add it to your +`analysis_options.yaml` under `bloc` > `rules`: + + diff --git a/packages/bloc_lint/README.md b/packages/bloc_lint/README.md index 636093d53b4..ebc46b774a2 100644 --- a/packages/bloc_lint/README.md +++ b/packages/bloc_lint/README.md @@ -96,6 +96,7 @@ For more information, check out the [official documentation](https://bloclibrary ## All Lint Rules +- [avoid_bloc_to_bloc_members](https://bloclibrary.dev/lint-rules/avoid_bloc_to_bloc_members) - [avoid_build_context_extensions](https://bloclibrary.dev/lint-rules/avoid_build_context_extensions) - [avoid_flutter_imports](https://bloclibrary.dev/lint-rules/avoid_flutter_imports) - [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods) diff --git a/packages/bloc_lint/lib/all.yaml b/packages/bloc_lint/lib/all.yaml index d7720e9a240..8f8ed7ac6ad 100644 --- a/packages/bloc_lint/lib/all.yaml +++ b/packages/bloc_lint/lib/all.yaml @@ -1,5 +1,6 @@ bloc: rules: + - avoid_bloc_to_bloc_members - avoid_build_context_extensions - avoid_flutter_imports - avoid_public_bloc_methods diff --git a/packages/bloc_lint/lib/bloc_lint.dart b/packages/bloc_lint/lib/bloc_lint.dart index 9db741ce5be..4840ce80f79 100644 --- a/packages/bloc_lint/lib/bloc_lint.dart +++ b/packages/bloc_lint/lib/bloc_lint.dart @@ -1,3 +1,7 @@ +export 'package:_fe_analyzer_shared/src/parser/formal_parameter_kind.dart' + show FormalParameterKind; +export 'package:_fe_analyzer_shared/src/parser/member_kind.dart' + show MemberKind; export 'package:_fe_analyzer_shared/src/parser/parser.dart' show DeclarationKind, IdentifierContext, Listener; export 'package:_fe_analyzer_shared/src/scanner/token.dart' @@ -8,6 +12,7 @@ export 'src/lint_rule.dart' show LintRule, LintRuleBuilder; export 'src/linter.dart' show LintContext, Linter; export 'src/rules/rules.dart' show + AvoidBlocToBlocMembers, AvoidBuildContextExtensions, AvoidFlutterImports, AvoidPublicBlocMethods, diff --git a/packages/bloc_lint/lib/src/linter.dart b/packages/bloc_lint/lib/src/linter.dart index db24ec826d4..65219473b6f 100644 --- a/packages/bloc_lint/lib/src/linter.dart +++ b/packages/bloc_lint/lib/src/linter.dart @@ -21,6 +21,7 @@ import 'package:path/path.dart' as p; /// All supported lint rules. final allRules = { + AvoidBlocToBlocMembers.rule: AvoidBlocToBlocMembers.new, AvoidBuildContextExtensions.rule: AvoidBuildContextExtensions.new, AvoidFlutterImports.rule: AvoidFlutterImports.new, AvoidPublicBlocMethods.rule: AvoidPublicBlocMethods.new, diff --git a/packages/bloc_lint/lib/src/rules/avoid_bloc_to_bloc_members.dart b/packages/bloc_lint/lib/src/rules/avoid_bloc_to_bloc_members.dart new file mode 100644 index 00000000000..850c5c726f5 --- /dev/null +++ b/packages/bloc_lint/lib/src/rules/avoid_bloc_to_bloc_members.dart @@ -0,0 +1,136 @@ +import 'package:bloc_lint/bloc_lint.dart'; + +/// {@template avoid_bloc_to_bloc_members} +/// The avoid_bloc_to_bloc_members lint rule. +/// {@endtemplate} +class AvoidBlocToBlocMembers extends LintRule { + /// {@macro avoid_bloc_to_bloc_members} + AvoidBlocToBlocMembers([Severity? severity]) + : super(name: rule, severity: severity ?? Severity.warning); + + /// The name of the lint rule. + static const rule = 'avoid_bloc_to_bloc_members'; + + @override + Listener? create(LintContext context) => _Listener(context); +} + +class _Listener extends Listener { + _Listener(this.context); + + final LintContext context; + + bool _isRelevantEnclosingClass = false; + + @override + void beginClassDeclaration( + Token begin, + Token? abstractToken, + Token? macroToken, + Token? sealedToken, + Token? baseToken, + Token? interfaceToken, + Token? finalToken, + Token? augmentToken, + Token? mixinToken, + Token name, + ) { + _isRelevantEnclosingClass = false; + + final extendz = name.next; + + if (extendz == null || extendz.kind != Keyword.EXTENDS.kind) return; + + final superclazz = extendz.next; + if (superclazz == null) return; + + if (_isBlocLike(superclazz)) { + _isRelevantEnclosingClass = true; + } + return; + } + + @override + void endClassFields( + Token? abstractToken, + Token? augmentToken, + Token? externalToken, + Token? staticToken, + Token? covariantToken, + Token? lateToken, + Token? varFinalOrConst, + int count, + Token beginToken, + Token endToken, + ) { + if (!_isRelevantEnclosingClass) return; + if (staticToken != null) return; + + final fieldName = _getFieldName(beginToken, endToken); + if (!_isBlocLike(fieldName)) return; + + context.reportTokenRange( + beginToken: beginToken, + endToken: endToken, + message: 'Avoid bloc or cubit members of blocs or cubits.', + hint: 'Prefer pushing the problem into the presentation or domain layer.', + ); + } + + @override + void endFormalParameter( + Token? thisKeyword, + Token? superKeyword, + Token? periodAfterThisOrSuper, + Token nameToken, + Token? initializerStart, + Token? initializerEnd, + FormalParameterKind kind, + MemberKind memberKind, + ) { + if (!_isRelevantEnclosingClass) return; + + final typeAnnotation = _getParameterTypeAnnotation(nameToken); + if (typeAnnotation == null) return; + if (!_isBlocLike(typeAnnotation)) return; + + context.reportTokenRange( + beginToken: typeAnnotation, + endToken: nameToken, + message: 'Avoid bloc or cubit members of blocs or cubits.', + hint: 'Prefer pushing the problem into the presentation or domain layer.', + ); + } +} + +bool _isBlocLike(Token token) => + token.lexeme.endsWith('Bloc') || token.lexeme.endsWith('Cubit'); + +List _getTokens(Token begin, Token end) { + final tokens = []; + Token? token = begin; + while (token != null && token != end) { + tokens.add(token); + token = token.next; + } + return tokens; +} + +Token _getFieldName(Token begin, Token end) { + final tokens = _getTokens(begin, end); + final equalsIndex = tokens.indexWhere((token) => token.type == TokenType.EQ); + if (equalsIndex != -1) return tokens.elementAt(equalsIndex).previous!; + return end.previous!; +} + +Token? _getParameterTypeAnnotation(Token? formalParameter) { + final previous = formalParameter?.previous; + if (previous?.type == TokenType.QUESTION) { + // FooBloc? foo + // ^^^^^^^ + return previous?.previous; + } + // FooBloc foo + // ^^^^^^^ + return previous; +} diff --git a/packages/bloc_lint/lib/src/rules/rules.dart b/packages/bloc_lint/lib/src/rules/rules.dart index cf15d858171..1de572132d7 100644 --- a/packages/bloc_lint/lib/src/rules/rules.dart +++ b/packages/bloc_lint/lib/src/rules/rules.dart @@ -1,3 +1,4 @@ +export 'avoid_bloc_to_bloc_members.dart'; export 'avoid_build_context_extensions.dart'; export 'avoid_flutter_imports.dart'; export 'avoid_public_bloc_methods.dart'; diff --git a/packages/bloc_lint/test/src/rules/avoid_bloc_to_bloc_members_test.dart b/packages/bloc_lint/test/src/rules/avoid_bloc_to_bloc_members_test.dart new file mode 100644 index 00000000000..dd75f7e64a0 --- /dev/null +++ b/packages/bloc_lint/test/src/rules/avoid_bloc_to_bloc_members_test.dart @@ -0,0 +1,189 @@ +import 'package:bloc_lint/bloc_lint.dart'; +import 'package:test/test.dart'; + +import '../lint_test_helper.dart'; + +void main() { + group(AvoidBlocToBlocMembers, () { + lintTest( + 'lints when bloc contains a bloc as a member', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + +class CounterBloc extends Bloc { + CounterBloc() : super(0); +} + +class CounterSquaredBloc extends Bloc { + final CounterBloc counterBloc; + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + CounterSquaredBloc(required this.counterBloc) : super(0); +} +''', + ); + + lintTest( + 'lints when bloc contains a cubit as a member', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + +class CounterCubit extends Cubit { + CounterCubit() : super(0); +} + +class CounterSquaredBloc extends Bloc { + final CounterCubit counterCubit; + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + CounterSquaredBloc(required this.counterCubit) : super(0); +} +''', + ); + + lintTest( + 'lints when bloc contains a bloc as a positional constructor parameter', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + + +class CounterBloc extends Bloc { + CounterBloc() : super(0); +} + +class CounterSquaredBloc extends Bloc { + CounterSquaredBloc(CounterBloc counterBloc) : super(counterBloc.state * counterBloc.state); + ^^^^^^^^^^^^^^^^^^^^^^^ +} +''', + ); + + lintTest( + 'lints when bloc contains a cubit as a positional constructor parameter', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + + +class CounterCubit extends Cubit { + CounterCubit() : super(0); +} + +class CounterSquaredBloc extends Bloc { + CounterSquaredBloc(CounterCubit counterCubit) : super(counterCubit.state * counterCubit.state); + ^^^^^^^^^^^^^^^^^^^^^^^^^ +} +''', + ); + + lintTest( + 'lints when bloc contains multiple positional bloc parameters', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + + +class CounterBloc extends Bloc { + CounterBloc() : super(0); +} + +class CounterMultiplyBloc extends Bloc { + CounterMultiplyBloc( + CounterBloc counterBloc1, + ^^^^^^^^^^^^^^^^^^^^^^^^ + CounterBloc? counterBloc2, + ^^^^^^^^^^^^^^^^^^^^^^^^^ + ) : super(counterBloc1.state * counterBloc2.state); +} +''', + ); + + lintTest( + 'lints when bloc contains a an optional bloc named constructor parameter', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + + +class CounterBloc extends Bloc { + CounterBloc() : super(0); +} + +class CounterSquaredBloc extends Bloc { + CounterSquaredBloc({ CounterBloc? counterBloc }) : super(counterBloc.state * counterBloc.state); + ^^^^^^^^^^^^^^^^^^^^^^^^ +} +''', + ); + + lintTest( + 'lints when bloc contains a an cubit parameter with a default', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + + +class CounterCubit extends Cubit { + CounterCubit() : super(0); +} + +class CounterSquaredBloc extends Bloc { + CounterSquaredBloc({ CounterCubit counterCubit = CounterCubit() }) : super(counterBloc.state * counterBloc.state); + ^^^^^^^^^^^^^^^^^^^^^^^^^ +} +''', + ); + + lintTest( + 'lints when bloc contains required named bloc constructor parameter', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + + +class CounterBloc extends Bloc { + CounterBloc() : super(0); +} + +class CounterSquaredBloc extends Bloc { + CounterSquaredBloc({ required CounterBloc counterBloc }) : super(counterBloc.state * counterBloc.state); + ^^^^^^^^^^^^^^^^^^^^^^^ +} +''', + ); + + lintTest( + 'lints when bloc contains multiple named bloc constructor parameters', + rule: AvoidBlocToBlocMembers.new, + path: 'counter_bloc.dart', + content: ''' +import 'package:bloc/bloc.dart'; + + +class CounterBloc extends Bloc { + CounterBloc() : super(0); +} + +class CounterSquaredBloc extends Bloc { + CounterSquaredBloc({ + required CounterBloc counterBloc1, + ^^^^^^^^^^^^^^^^^^^^^^^^ + CounterBloc? counterBloc2 + ^^^^^^^^^^^^^^^^^^^^^^^^^ + }) : super(counterBloc1.state * counterBloc2.state); +} +''', + ); + }); +}