Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Code } from '@astrojs/starlight/components';
import { transformerMetaHighlight } from '@shikijs/transformers';

<Code
code={`
import 'package:bloc/bloc.dart';
import 'package:notification_bloc/notification_bloc.dart';

enum CounterEvent { increment };

class CounterBloc extends Bloc<CounterEvent, int> {
// Avoid blocs as members
final NotificationBloc _notificationBloc;

CounterBloc(this._notificationBloc) : super(0) {
on<CounterEvent>((event, emit) {
emit(state + 1);
_notificationBloc.add(NotificationEvent(message: 'Counter value incremented'));
});
}
}
`}
lang="dart" title="counter_page.dart"
transformers={[transformerMetaHighlight()]} class='warning' meta="{8}"
/>
Original file line number Diff line number Diff line change
@@ -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<CounterBloc, CounterState>(
listener: (context, state) {
context.read<NotificationBloc>().add(NotificationEvent(message: 'Counter value incremented'));
},
child: child,
);
}
}
`;
---

<Code code={code} lang="dart" title="my_notifier.dart" />
44 changes: 44 additions & 0 deletions docs/src/content/docs/lint-rules/avoid_bloc_to_bloc_members.mdx
Original file line number Diff line number Diff line change
@@ -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';

<div class="badges">
<Badge text="new" />
<Badge text="dart" variant="note" />
</div>

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**:

<BadSnippet />

**GOOD**:

<GoodSnippet />

## Enable

To enable the `avoid_bloc_to_bloc_members` rule, add it to your
`analysis_options.yaml` under `bloc` > `rules`:

<EnableRuleSnippet name="avoid_bloc_to_bloc_members" />
1 change: 1 addition & 0 deletions packages/bloc_lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/all.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
bloc:
rules:
- avoid_bloc_to_bloc_members
- avoid_build_context_extensions
- avoid_flutter_imports
- avoid_public_bloc_methods
Expand Down
5 changes: 5 additions & 0 deletions packages/bloc_lint/lib/bloc_lint.dart
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/src/linter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:path/path.dart' as p;

/// All supported lint rules.
final allRules = <String, LintRuleBuilder>{
AvoidBlocToBlocMembers.rule: AvoidBlocToBlocMembers.new,
AvoidBuildContextExtensions.rule: AvoidBuildContextExtensions.new,
AvoidFlutterImports.rule: AvoidFlutterImports.new,
AvoidPublicBlocMethods.rule: AvoidPublicBlocMethods.new,
Expand Down
136 changes: 136 additions & 0 deletions packages/bloc_lint/lib/src/rules/avoid_bloc_to_bloc_members.dart
Original file line number Diff line number Diff line change
@@ -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<Token> _getTokens(Token begin, Token end) {
final tokens = <Token>[];
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;
}
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/src/rules/rules.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Loading