Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

Commit 12bffdf

Browse files
authored
feat: introduce new code diagnostics prefer-single-widget-per-file (#406)
1 parent 8800440 commit 12bffdf

File tree

13 files changed

+379
-0
lines changed

13 files changed

+379
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unresolved
4+
5+
* Add static code diagnostic `prefer-single-widget-per-file`.
6+
37
## 4.1.0
48

59
* Add better monorepos support for CLI

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ Rules configuration is [described here](#configuring-a-rules-entry).
358358
- [avoid-unnecessary-setstate](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/avoid-unnecessary-setstate.md)
359359
- [avoid-wrapping-in-padding](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/avoid-wrapping-in-padding.md)
360360
- [prefer-extracting-callbacks](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-extracting-callbacks.md)   [![Configurable](https://img.shields.io/badge/-configurable-informational)](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-extracting-callbacks.md#config-example)
361+
- [prefer-single-widget-per-file](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-single-widget-per-file.md)   [![Configurable](https://img.shields.io/badge/-configurable-informational)](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-single-widget-per-file.md#config-example)
361362
362363
### Intl specific
363364
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Prefer single widget per file
2+
3+
![Configurable](https://img.shields.io/badge/-configurable-informational)
4+
5+
## Rule id
6+
7+
prefer-single-widget-per-file
8+
9+
## Description
10+
11+
Warns when a file contains more than a single widget.
12+
13+
Ensures that files have a single responsibility so that each widget exists in its own file.
14+
15+
### Config example
16+
17+
```yaml
18+
dart_code_metrics:
19+
...
20+
rules:
21+
...
22+
- prefer-single-widget-per-file:
23+
ignore-private-widgets: true
24+
```
25+
26+
### Example
27+
28+
Bad:
29+
30+
some_widgets.dart
31+
32+
```dart
33+
class SomeWidget extends StatelessWidget {
34+
@override
35+
Widget build(BuildContext context) {
36+
...
37+
}
38+
}
39+
40+
// LINT
41+
class SomeOtherWidget extends StatelessWidget {
42+
@override
43+
Widget build(BuildContext context) {
44+
...
45+
}
46+
}
47+
48+
// LINT
49+
class _SomeOtherWidget extends StatelessWidget {
50+
@override
51+
Widget build(BuildContext context) {
52+
...
53+
}
54+
}
55+
56+
// LINT
57+
class SomeStatefulWidget extends StatefulWidget {
58+
@override
59+
_SomeStatefulWidgetState createState() => _someStatefulWidgetState();
60+
}
61+
62+
class _SomeStatefulWidgetState extends State<InspirationCard> {
63+
@override
64+
Widget build(BuildContext context) {
65+
...
66+
}
67+
}
68+
```
69+
70+
Good:
71+
72+
some_widget.dart
73+
74+
```dart
75+
class SomeWidget extends StatelessWidget {
76+
@override
77+
Widget build(BuildContext context) {
78+
...
79+
}
80+
}
81+
```
82+
83+
some_other_widget.dart
84+
85+
```dart
86+
class SomeOtherWidget extends StatelessWidget {
87+
@override
88+
Widget build(BuildContext context) {
89+
...
90+
}
91+
}
92+
```

lib/src/analyzers/lint_analyzer/rules/rules_factory.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import 'rules_list/prefer_conditional_expressions/prefer_conditional_expressions
2323
import 'rules_list/prefer_extracting_callbacks/prefer_extracting_callbacks.dart';
2424
import 'rules_list/prefer_intl_name/prefer_intl_name.dart';
2525
import 'rules_list/prefer_on_push_cd_strategy/prefer_on_push_cd_strategy.dart';
26+
import 'rules_list/prefer_single_widget_per_file/prefer_single_widget_per_file.dart';
2627
import 'rules_list/prefer_trailing_comma/prefer_trailing_comma.dart';
2728
import 'rules_list/provide_correct_intl_args/provide_correct_intl_args.dart';
2829

@@ -64,6 +65,8 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
6465
PreferIntlNameRule.ruleId: (config) => PreferIntlNameRule(config),
6566
PreferOnPushCdStrategyRule.ruleId: (config) =>
6667
PreferOnPushCdStrategyRule(config),
68+
PreferSingleWidgetPerFileRule.ruleId: (config) =>
69+
PreferSingleWidgetPerFileRule(config),
6770
PreferTrailingCommaRule.ruleId: (config) => PreferTrailingCommaRule(config),
6871
ProvideCorrectIntlArgsRule.ruleId: (config) =>
6972
ProvideCorrectIntlArgsRule(config),
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
part of 'prefer_single_widget_per_file.dart';
2+
3+
class _ConfigParser {
4+
static const _ignorePrivateWidgetsName = 'ignore-private-widgets';
5+
6+
static bool parseIgnorePrivateWidgets(Map<String, Object> config) =>
7+
config[_ignorePrivateWidgetsName] == true;
8+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/visitor.dart';
3+
4+
import '../../../../../utils/node_utils.dart';
5+
import '../../../models/internal_resolved_unit_result.dart';
6+
import '../../../models/issue.dart';
7+
import '../../../models/severity.dart';
8+
import '../../flutter_rule_utils.dart';
9+
import '../../models/rule.dart';
10+
import '../../models/rule_documentation.dart';
11+
import '../../rule_utils.dart';
12+
13+
part 'config_parser.dart';
14+
part 'visitor.dart';
15+
16+
class PreferSingleWidgetPerFileRule extends Rule {
17+
static const String ruleId = 'prefer-single-widget-per-file';
18+
19+
static const _warningMessage = 'Only a single widget per file is allowed.';
20+
21+
final bool _ignorePrivateWidgets;
22+
23+
PreferSingleWidgetPerFileRule([Map<String, Object> config = const {}])
24+
: _ignorePrivateWidgets = _ConfigParser.parseIgnorePrivateWidgets(config),
25+
super(
26+
id: ruleId,
27+
documentation: const RuleDocumentation(
28+
name: 'Prefer a single widget per file',
29+
brief: 'Warns when a file contains more than a single widget.',
30+
),
31+
severity: readSeverity(config, Severity.style),
32+
excludes: readExcludes(config),
33+
);
34+
35+
@override
36+
Iterable<Issue> check(InternalResolvedUnitResult source) {
37+
final visitor = _Visitor(ignorePrivateWidgets: _ignorePrivateWidgets);
38+
39+
source.unit.visitChildren(visitor);
40+
41+
return visitor.nodes
42+
.map(
43+
(node) => createIssue(
44+
rule: this,
45+
location: nodeLocation(
46+
node: node,
47+
source: source,
48+
),
49+
message: _warningMessage,
50+
),
51+
)
52+
.toList(growable: false);
53+
}
54+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
part of 'prefer_single_widget_per_file.dart';
2+
3+
class _Visitor extends SimpleAstVisitor<void> {
4+
final bool _ignorePrivateWidgets;
5+
6+
final _nodes = <ClassDeclaration>[];
7+
8+
_Visitor({required bool ignorePrivateWidgets})
9+
: _ignorePrivateWidgets = ignorePrivateWidgets;
10+
11+
Iterable<ClassDeclaration> get nodes =>
12+
_nodes.length > 1 ? _nodes.skip(1) : [];
13+
14+
@override
15+
void visitClassDeclaration(ClassDeclaration node) {
16+
super.visitClassDeclaration(node);
17+
18+
final classType = node.extendsClause?.superclass.type;
19+
if (isWidgetOrSubclass(classType) &&
20+
(!_ignorePrivateWidgets || !Identifier.isPrivateName(node.name.name))) {
21+
_nodes.add(node);
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import 'flutter_defines.dart';
2+
3+
class SomeStatefulWidget extends StatefulWidget {
4+
@override
5+
_someStatefulWidgetState createState() => _someStatefulWidgetState();
6+
}
7+
8+
class _SomeStatefulWidgetState extends State<InspirationCard> {
9+
@override
10+
Widget build(BuildContext context) {
11+
// ...
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import 'flutter_defines.dart';
2+
3+
class SomeWidget extends StatelessWidget {
4+
@override
5+
Widget build(BuildContext context) {
6+
// ...
7+
}
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class Widget {}
2+
3+
class StatefulWidget extends Widget {}
4+
5+
class StatelessWidget extends Widget {}

0 commit comments

Comments
 (0)