Skip to content

Commit 3a26219

Browse files
Add the bloc_related_class_naming lint (#504)
* Add the `bloc_related_class_naming` lint * Add tests for classes in parts * Update note * Add a doc comment * Simplify and allow configuring suffixes * Don't pass entire config around
1 parent 6e29c11 commit 3a26219

12 files changed

Lines changed: 505 additions & 21 deletions

packages/leancode_lint/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,59 @@ None.
218218

219219
</details>
220220

221+
<details>
222+
<summary>`bloc_related_class_naming`</summary>
223+
224+
### `bloc_related_class_naming`
225+
226+
**DO** follow the naming convention for Bloc/Cubit related classes.
227+
228+
For `ExampleBloc`:
229+
- Event class: `ExampleEvent`
230+
- State class: `ExampleState`
231+
- Presentation Event class: `ExamplePresentationEvent`
232+
233+
For `ExampleCubit`:
234+
- State class: `ExampleState`
235+
- Presentation Event class: `ExamplePresentationEvent`
236+
237+
> [!NOTE]
238+
> This lint only checks classes defined in the same library (including parts) as the Bloc/Cubit.
239+
> Presentation events are only checked if the `bloc_presentation` package is used.
240+
241+
**BAD:**
242+
243+
```dart
244+
class MyBloc extends Bloc<WrongEvent, WrongState> {}
245+
```
246+
247+
**GOOD:**
248+
249+
```dart
250+
class MyBloc extends Bloc<MyEvent, MyState> {}
251+
```
252+
253+
#### Configuration
254+
255+
Configured via `LeanCodeLintConfig.blocRelatedClassNaming`:
256+
257+
```dart
258+
import 'package:leancode_lint/plugin.dart';
259+
260+
final plugin = LeanCodeLintPlugin(
261+
name: 'my_lints',
262+
config: LeanCodeLintConfig(
263+
blocRelatedClassNaming: BlocRelatedClassNamingConfig(
264+
stateSuffix: 'State',
265+
eventSuffix: 'Event',
266+
presentationEventSuffix: 'PresentationEvent',
267+
),
268+
),
269+
);
270+
```
271+
272+
</details>
273+
221274
<details>
222275
<summary><code>catch_parameter_names</code></summary>
223276

packages/leancode_lint/lib/config.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ final class LeanCodeLintConfig {
33
this.applicationPrefix,
44
this.designSystemItemReplacements = const {},
55
this.catchParameterNames = const .new(),
6+
this.blocRelatedClassNaming = const .new(),
67
});
78

89
/// Used by some rules (e.g. `prefix_widgets_returning_slivers`) to match
@@ -19,6 +20,30 @@ final class LeanCodeLintConfig {
1920

2021
/// Configuration for the `catch_parameter_names` rule.
2122
final CatchParameterNamesConfig catchParameterNames;
23+
24+
/// Configuration for the `bloc_related_class_naming` rule.
25+
final BlocRelatedClassNamingConfig blocRelatedClassNaming;
26+
}
27+
28+
/// Configuration for the `bloc_related_class_naming` rule.
29+
///
30+
/// Each suffix is appended to the BLoC/Cubit subject name (the part before
31+
/// `Bloc` or `Cubit`) to form the expected class name.
32+
///
33+
/// For example, for `FooBloc` the default expected names are:
34+
/// - state → `FooState`
35+
/// - event → `FooEvent`
36+
/// - presentation event → `FooPresentationEvent`
37+
class BlocRelatedClassNamingConfig {
38+
const BlocRelatedClassNamingConfig({
39+
this.stateSuffix = 'State',
40+
this.eventSuffix = 'Event',
41+
this.presentationEventSuffix = 'PresentationEvent',
42+
});
43+
44+
final String stateSuffix;
45+
final String eventSuffix;
46+
final String presentationEventSuffix;
2247
}
2348

2449
class CatchParameterNamesConfig {

packages/leancode_lint/lib/plugin.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:leancode_lint/src/assists/convert_record_into_nominal_type.dart'
77
import 'package:leancode_lint/src/lints/add_cubit_suffix_for_cubits.dart';
88
import 'package:leancode_lint/src/lints/avoid_conditional_hooks.dart';
99
import 'package:leancode_lint/src/lints/avoid_single_child_in_multi_child_widget.dart';
10+
import 'package:leancode_lint/src/lints/bloc_related_class_naming.dart';
1011
import 'package:leancode_lint/src/lints/catch_parameter_names.dart';
1112
import 'package:leancode_lint/src/lints/constructor_parameters_and_fields_should_have_the_same_order.dart';
1213
import 'package:leancode_lint/src/lints/hook_widget_does_not_use_hooks.dart';
@@ -40,13 +41,22 @@ final class LeanCodeLintPlugin extends Plugin {
4041
).forEach(registry.registerWarningRule);
4142
registry
4243
..registerWarningRule(StartCommentsWithSpace())
43-
..registerWarningRule(PrefixWidgetsReturningSlivers(config: config))
44+
..registerWarningRule(
45+
PrefixWidgetsReturningSlivers(
46+
applicationPrefix: config.applicationPrefix,
47+
),
48+
)
4449
..registerFixForRule(
4550
StartCommentsWithSpace.code,
4651
AddStartingSpaceToComment.new,
4752
)
4853
..registerWarningRule(AddCubitSuffixForYourCubits())
49-
..registerWarningRule(CatchParameterNames(config: config))
54+
..registerWarningRule(
55+
BlocRelatedClassNaming(config: config.blocRelatedClassNaming),
56+
)
57+
..registerWarningRule(
58+
CatchParameterNames(config: config.catchParameterNames),
59+
)
5060
..registerWarningRule(AvoidConditionalHooks())
5161
..registerWarningRule(HookWidgetDoesNotUseHooks())
5262
..registerFixForRule(
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/element/element.dart';
3+
import 'package:leancode_lint/src/type_checker.dart';
4+
5+
const blocChecker = TypeChecker.fromName('Bloc', packageName: 'bloc');
6+
7+
const cubitChecker = TypeChecker.fromName('Cubit', packageName: 'bloc');
8+
9+
const blocPresentationMixinChecker = TypeChecker.fromName(
10+
'BlocPresentationMixin',
11+
packageName: 'bloc_presentation',
12+
);
13+
14+
String? getBlocSubject(String className, {required BlocType blocType}) =>
15+
switch (blocType) {
16+
.bloc when className.endsWith('Bloc') => className.substring(
17+
0,
18+
className.length - 4,
19+
),
20+
.cubit when className.endsWith('Cubit') => className.substring(
21+
0,
22+
className.length - 5,
23+
),
24+
_ => null,
25+
};
26+
27+
enum BlocType { bloc, cubit }
28+
29+
BlocType? determineBlocType(Element? element) {
30+
if (element == null) {
31+
return null;
32+
}
33+
34+
if (blocChecker.isAssignableFrom(element)) {
35+
return .bloc;
36+
} else if (cubitChecker.isAssignableFrom(element)) {
37+
return .cubit;
38+
}
39+
40+
return null;
41+
}
42+
43+
class BlocInfo {
44+
const BlocInfo({
45+
required this.type,
46+
this.stateType,
47+
this.eventType,
48+
this.presentationEventType,
49+
});
50+
51+
final BlocType type;
52+
final TypeAnnotation? stateType;
53+
final TypeAnnotation? eventType;
54+
final TypeAnnotation? presentationEventType;
55+
}
56+
57+
BlocInfo? getBlocInfo(ClassDeclaration node) {
58+
final extendsClause = node.extendsClause;
59+
final superclass = extendsClause?.superclass;
60+
final superclassElement = superclass?.element;
61+
62+
final type = determineBlocType(superclassElement);
63+
if (type == null) {
64+
return null;
65+
}
66+
67+
final typeArguments = superclass?.typeArguments?.arguments;
68+
69+
final (eventType, stateType) = switch (typeArguments) {
70+
[final state] when type == .cubit => (null, state),
71+
[final event, final state] when type == .bloc => (event, state),
72+
_ => (null, null),
73+
};
74+
75+
TypeAnnotation? presentationEventType;
76+
if (node.withClause case final withClause?) {
77+
for (final mixin in withClause.mixinTypes) {
78+
if (mixin.element case final mixinElement?
79+
when blocPresentationMixinChecker.isExactly(mixinElement)) {
80+
if (mixin.typeArguments?.arguments case [_, final presentationEvent]) {
81+
presentationEventType = presentationEvent;
82+
}
83+
}
84+
}
85+
}
86+
87+
return .new(
88+
type: type,
89+
stateType: stateType,
90+
eventType: eventType,
91+
presentationEventType: presentationEventType,
92+
);
93+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import 'package:analyzer/analysis_rule/analysis_rule.dart';
2+
import 'package:analyzer/analysis_rule/rule_context.dart';
3+
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
4+
import 'package:analyzer/dart/ast/ast.dart';
5+
import 'package:analyzer/dart/ast/visitor.dart';
6+
import 'package:analyzer/error/error.dart';
7+
import 'package:leancode_lint/config.dart';
8+
import 'package:leancode_lint/src/bloc_utils.dart';
9+
import 'package:leancode_lint/src/utils.dart';
10+
11+
/// Enforces consistent naming of state, event, and presentation event classes
12+
/// related to a BLoC or Cubit.
13+
///
14+
/// Given a BLoC or Cubit named `FooBloc` or `FooCubit`, the associated classes
15+
/// should be named:
16+
/// - state → `FooState`
17+
/// - event → `FooEvent`
18+
/// - presentation event → `FooPresentationEvent`
19+
///
20+
/// The suffixes are configurable via [BlocRelatedClassNamingConfig].
21+
class BlocRelatedClassNaming extends AnalysisRule {
22+
BlocRelatedClassNaming({this.config = const .new()})
23+
: super(name: code.lowerCaseName, description: code.problemMessage);
24+
25+
final BlocRelatedClassNamingConfig config;
26+
27+
static const code = LintCode(
28+
'bloc_related_class_naming',
29+
"The name of {0}'s {1} should be {2}.",
30+
severity: .WARNING,
31+
);
32+
33+
@override
34+
LintCode get diagnosticCode => code;
35+
36+
@override
37+
void registerNodeProcessors(
38+
RuleVisitorRegistry registry,
39+
RuleContext context,
40+
) {
41+
registry.addClassDeclaration(this, _Visitor(this, context, config));
42+
}
43+
}
44+
45+
class _Visitor extends SimpleAstVisitor<void> {
46+
_Visitor(this.rule, this.context, this.config);
47+
48+
final AnalysisRule rule;
49+
final RuleContext context;
50+
final BlocRelatedClassNamingConfig config;
51+
52+
@override
53+
void visitClassDeclaration(ClassDeclaration node) {
54+
final blocInfo = getBlocInfo(node);
55+
if (blocInfo == null) {
56+
return;
57+
}
58+
59+
final classElement = node.declaredFragment?.element;
60+
final className = node.namePart.typeName.lexeme;
61+
final subject = getBlocSubject(className, blocType: blocInfo.type);
62+
63+
if (subject == null) {
64+
return;
65+
}
66+
67+
void checkName(TypeAnnotation type, String classType, String suffix) {
68+
final expectedName = '$subject$suffix';
69+
70+
if (type case NamedType(
71+
:final name,
72+
:final element?,
73+
:final CompilationUnit root,
74+
) when name.lexeme != expectedName) {
75+
if (element.library != classElement?.library) {
76+
return;
77+
}
78+
79+
final declaration = root.declarations
80+
.whereType<ClassDeclaration>()
81+
.firstWhereOrNull((d) => d.declaredFragment?.element == element);
82+
83+
rule.reportAtToken(
84+
declaration?.namePart.typeName ?? name,
85+
arguments: [className, classType, expectedName],
86+
);
87+
}
88+
}
89+
90+
if (blocInfo.stateType case final stateType?) {
91+
checkName(stateType, 'state', config.stateSuffix);
92+
}
93+
94+
if (blocInfo.eventType case final eventType?) {
95+
checkName(eventType, 'event', config.eventSuffix);
96+
}
97+
98+
if (blocInfo.presentationEventType case final presentationEventType?) {
99+
checkName(
100+
presentationEventType,
101+
'presentation event',
102+
config.presentationEventSuffix,
103+
);
104+
}
105+
}
106+
}

packages/leancode_lint/lib/src/lints/catch_parameter_names.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class CatchParameterNames extends AnalysisRule {
2020
CatchParameterNames({required this.config})
2121
: super(name: code.lowerCaseName, description: code.problemMessage);
2222

23-
final LeanCodeLintConfig config;
23+
final CatchParameterNamesConfig config;
2424

2525
static const code = LintCode(
2626
'catch_parameter_names',
@@ -46,7 +46,7 @@ class _Visitor extends SimpleAstVisitor<void> {
4646

4747
final AnalysisRule rule;
4848
final RuleContext context;
49-
final LeanCodeLintConfig config;
49+
final CatchParameterNamesConfig config;
5050

5151
@override
5252
void visitCatchClause(CatchClause node) {
@@ -81,8 +81,8 @@ enum _CatchClauseParameter {
8181
exception,
8282
stackTrace;
8383

84-
String preferredName(LeanCodeLintConfig config) => switch (this) {
85-
exception => config.catchParameterNames.exception,
86-
stackTrace => config.catchParameterNames.stackTrace,
84+
String preferredName(CatchParameterNamesConfig config) => switch (this) {
85+
exception => config.exception,
86+
stackTrace => config.stackTrace,
8787
};
8888
}

0 commit comments

Comments
 (0)