Skip to content

Commit 3b8ad69

Browse files
committed
feat(bloc_lint): add avoid_bloc_to_bloc_members
1 parent ad6c4a1 commit 3b8ad69

10 files changed

Lines changed: 426 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Code } from '@astrojs/starlight/components';
2+
import { transformerMetaHighlight } from '@shikijs/transformers';
3+
4+
<Code
5+
code={`
6+
import 'package:bloc/bloc.dart';
7+
import 'package:notification_bloc/notification_bloc.dart';
8+
9+
enum CounterEvent { increment };
10+
11+
class CounterBloc extends Bloc<CounterEvent, int> {
12+
// Avoid blocs as members
13+
final NotificationBloc _notificationBloc;
14+
15+
CounterBloc(this._notificationBloc) : super(0) {
16+
on<CounterEvent>((event, emit) {
17+
emit(state + 1);
18+
_notificationBloc.add(NotificationEvent(message: 'Counter value incremented'));
19+
});
20+
}
21+
}
22+
`}
23+
lang="dart" title="counter_page.dart"
24+
transformers={[transformerMetaHighlight()]} class='warning' meta="{8}"
25+
/>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
import { Code } from '@astrojs/starlight/components';
3+
4+
const code = `
5+
import '../bloc/counter_bloc.dart';
6+
import '../bloc/notification_bloc.dart';
7+
8+
class MyNotifier extends StatelessWidget {
9+
final Widget child;
10+
const MyNotifier({super.key, required this.child});
11+
12+
@override
13+
Widget build(BuildContext context) {
14+
// Handle at the application layer
15+
return BlocListener<CounterBloc, CounterState>(
16+
listener: (context, state) {
17+
context.read<NotificationBloc>().add(NotificationEvent(message: 'Counter value incremented'));
18+
},
19+
child: child,
20+
);
21+
}
22+
}
23+
`;
24+
---
25+
26+
<Code code={code} lang="dart" title="my_notifier.dart" />
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
title: Avoid Bloc-to-Bloc Members
3+
description: The avoid_bloc_to_bloc_members rule
4+
---
5+
6+
import { Badge } from '@astrojs/starlight/components';
7+
import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro';
8+
import BadSnippet from '~/components/lint-rules/avoid_bloc_to_bloc_members/BadSnippet.mdx';
9+
import GoodSnippet from '~/components/lint-rules/avoid_bloc_to_bloc_members/GoodSnippet.astro';
10+
11+
<div class="badges">
12+
<Badge text="new" />
13+
<Badge text="dart" variant="note" />
14+
</div>
15+
16+
Avoid including `Bloc`s or `Cubit`s as members of other `Bloc`s or `Cubit`s.
17+
18+
## Rationale
19+
20+
While passing blocs to other blocs may be error free, it creates dependencies between blocs.
21+
22+
See also [Bloc-to-Bloc Communication](https://bloclibrary.dev/architecture/#bloc-to-bloc-communication)
23+
24+
## Examples
25+
26+
**Avoid** passing a `Bloc` or `Cubit` to the constructor of another `Bloc` or `Cubit`.
27+
28+
**BAD**:
29+
30+
<BadSnippet />
31+
32+
**GOOD**:
33+
34+
<GoodSnippet />
35+
36+
## Enable
37+
38+
To enable the `avoid_bloc_to_bloc_members` rule, add it to your
39+
`analysis_options.yaml` under `bloc` > `rules`:
40+
41+
<EnableRuleSnippet name="avoid_bloc_to_bloc_members" />

packages/bloc_lint/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ For more information, check out the [official documentation](https://bloclibrary
9696

9797
## All Lint Rules
9898

99+
- [avoid_bloc_to_bloc_members](https://bloclibrary.dev/lint-rules/avoid_bloc_to_bloc_members)
99100
- [avoid_build_context_extensions](https://bloclibrary.dev/lint-rules/avoid_build_context_extensions)
100101
- [avoid_flutter_imports](https://bloclibrary.dev/lint-rules/avoid_flutter_imports)
101102
- [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods)

packages/bloc_lint/lib/all.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
bloc:
22
rules:
3+
- avoid_bloc_to_bloc_members
34
- avoid_build_context_extensions
45
- avoid_flutter_imports
56
- avoid_public_bloc_methods

packages/bloc_lint/lib/bloc_lint.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export 'package:_fe_analyzer_shared/src/parser/formal_parameter_kind.dart'
2+
show FormalParameterKind;
3+
export 'package:_fe_analyzer_shared/src/parser/member_kind.dart'
4+
show MemberKind;
15
export 'package:_fe_analyzer_shared/src/parser/parser.dart'
26
show DeclarationKind, IdentifierContext, Listener;
37
export 'package:_fe_analyzer_shared/src/scanner/token.dart'
@@ -8,6 +12,7 @@ export 'src/lint_rule.dart' show LintRule, LintRuleBuilder;
812
export 'src/linter.dart' show LintContext, Linter;
913
export 'src/rules/rules.dart'
1014
show
15+
AvoidBlocToBlocMembers,
1116
AvoidBuildContextExtensions,
1217
AvoidFlutterImports,
1318
AvoidPublicBlocMethods,

packages/bloc_lint/lib/src/linter.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:path/path.dart' as p;
1616

1717
/// All supported lint rules.
1818
final allRules = <String, LintRuleBuilder>{
19+
AvoidBlocToBlocMembers.rule: AvoidBlocToBlocMembers.new,
1920
AvoidBuildContextExtensions.rule: AvoidBuildContextExtensions.new,
2021
AvoidFlutterImports.rule: AvoidFlutterImports.new,
2122
AvoidPublicBlocMethods.rule: AvoidPublicBlocMethods.new,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import 'package:bloc_lint/bloc_lint.dart';
2+
3+
/// {@template avoid_bloc_to_bloc_members}
4+
/// The avoid_bloc_to_bloc_members lint rule.
5+
/// {@endtemplate}
6+
class AvoidBlocToBlocMembers extends LintRule {
7+
/// {@macro avoid_bloc_to_bloc_members}
8+
AvoidBlocToBlocMembers([Severity? severity])
9+
: super(name: rule, severity: severity ?? Severity.warning);
10+
11+
/// The name of the lint rule.
12+
static const rule = 'avoid_bloc_to_bloc_members';
13+
14+
@override
15+
Listener? create(LintContext context) => _Listener(context);
16+
}
17+
18+
class _Listener extends Listener {
19+
_Listener(this.context);
20+
21+
final LintContext context;
22+
23+
bool _isRelevantEnclosingClass = false;
24+
25+
@override
26+
void beginClassDeclaration(
27+
Token begin,
28+
Token? abstractToken,
29+
Token? macroToken,
30+
Token? sealedToken,
31+
Token? baseToken,
32+
Token? interfaceToken,
33+
Token? finalToken,
34+
Token? augmentToken,
35+
Token? mixinToken,
36+
Token name,
37+
) {
38+
_isRelevantEnclosingClass = false;
39+
40+
final extendz = name.next;
41+
42+
if (extendz == null || extendz.kind != Keyword.EXTENDS.kind) return;
43+
44+
final superclazz = extendz.next;
45+
if (superclazz == null) return;
46+
47+
if (_isBlocLike(superclazz)) {
48+
_isRelevantEnclosingClass = true;
49+
}
50+
return;
51+
}
52+
53+
@override
54+
void endClassFields(
55+
Token? abstractToken,
56+
Token? augmentToken,
57+
Token? externalToken,
58+
Token? staticToken,
59+
Token? covariantToken,
60+
Token? lateToken,
61+
Token? varFinalOrConst,
62+
int count,
63+
Token beginToken,
64+
Token endToken,
65+
) {
66+
if (!_isRelevantEnclosingClass) return;
67+
if (staticToken != null) return;
68+
69+
final fieldName = _getFieldName(beginToken, endToken);
70+
if (!_isBlocLike(fieldName)) return;
71+
72+
context.reportTokenRange(
73+
beginToken: beginToken,
74+
endToken: endToken,
75+
message: 'Avoid bloc or cubit members of blocs or cubits.',
76+
hint: 'Prefer pushing the problem into the presentation or domain layer.',
77+
);
78+
}
79+
80+
@override
81+
void endFormalParameter(
82+
Token? thisKeyword,
83+
Token? superKeyword,
84+
Token? periodAfterThisOrSuper,
85+
Token nameToken,
86+
Token? initializerStart,
87+
Token? initializerEnd,
88+
FormalParameterKind kind,
89+
MemberKind memberKind,
90+
) {
91+
if (!_isRelevantEnclosingClass) return;
92+
93+
final typeAnnotation = _getParameterTypeAnnotation(nameToken);
94+
if (typeAnnotation == null) return;
95+
if (!_isBlocLike(typeAnnotation)) return;
96+
97+
context.reportTokenRange(
98+
beginToken: typeAnnotation,
99+
endToken: nameToken,
100+
message: 'Avoid bloc or cubit members of blocs or cubits.',
101+
hint: 'Prefer pushing the problem into the presentation or domain layer.',
102+
);
103+
}
104+
}
105+
106+
bool _isBlocLike(Token token) =>
107+
token.lexeme.endsWith('Bloc') || token.lexeme.endsWith('Cubit');
108+
109+
List<Token> _getTokens(Token begin, Token end) {
110+
final tokens = <Token>[];
111+
Token? token = begin;
112+
while (token != null && token != end) {
113+
tokens.add(token);
114+
token = token.next;
115+
}
116+
return tokens;
117+
}
118+
119+
Token _getFieldName(Token begin, Token end) {
120+
final tokens = _getTokens(begin, end);
121+
final equalsIndex = tokens.indexWhere((token) => token.type == TokenType.EQ);
122+
if (equalsIndex != -1) return tokens.elementAt(equalsIndex).previous!;
123+
return end.previous!;
124+
}
125+
126+
Token? _getParameterTypeAnnotation(Token? formalParameter) {
127+
final previous = formalParameter?.previous;
128+
if (previous?.type == TokenType.QUESTION) {
129+
// FooBloc? foo
130+
// ^^^^^^^
131+
return previous?.previous;
132+
}
133+
// FooBloc foo
134+
// ^^^^^^^
135+
return previous;
136+
}

packages/bloc_lint/lib/src/rules/rules.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export 'avoid_bloc_to_bloc_members.dart';
12
export 'avoid_build_context_extensions.dart';
23
export 'avoid_flutter_imports.dart';
34
export 'avoid_public_bloc_methods.dart';

0 commit comments

Comments
 (0)