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

Commit 9fc2d1e

Browse files
authored
Prefer matching widget file name (#410)
1 parent 0768d8a commit 9fc2d1e

File tree

12 files changed

+312
-0
lines changed

12 files changed

+312
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ Rules configuration is [described here](#configuring-a-rules-entry).
349349
- [no-magic-number](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/no-magic-number.md)   [![Configurable](https://img.shields.io/badge/-configurable-informational)](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/no-magic-number.md#config-example)
350350
- [no-object-declaration](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/no-object-declaration.md)
351351
- [prefer-conditional-expressions](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-conditional-expressions.md)   ![Has auto-fix](https://img.shields.io/badge/-has%20auto--fix-success)
352+
- [prefer-match-file-name](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-match-file-name.md)
352353
- [prefer-trailing-comma](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-trailing-comma.md)   [![Configurable](https://img.shields.io/badge/-configurable-informational)](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/prefer-trailing-comma.md#config-example)   ![Has auto-fix](https://img.shields.io/badge/-has%20auto--fix-success)
353354
354355
### Flutter specific
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Prefer match file name
2+
3+
## Rule id
4+
5+
prefer-match-file-name
6+
7+
## Description
8+
9+
Warns if the file name does not match the name of the first public class in the file or a private class if there are no
10+
public classes.
11+
12+
### Config example
13+
14+
We recommend exclude the `test` folder.
15+
16+
```yaml
17+
dart_code_metrics:
18+
...
19+
rules:
20+
...
21+
- prefer-match-file-name:
22+
exclude:
23+
- test/**
24+
...
25+
```
26+
27+
### Example
28+
29+
#### Example 1 One class in the file
30+
31+
Bad:
32+
33+
File name: **some_widget.dart**
34+
35+
```dart
36+
class SomeOtherWidget extends StatelessWidget {
37+
@override
38+
Widget build(BuildContext context) {
39+
//...
40+
}
41+
}
42+
```
43+
44+
Good:
45+
46+
File name: **some_widget.dart**
47+
48+
```dart
49+
class SomeWidget extends StatelessWidget {
50+
@override
51+
Widget build(BuildContext context) {
52+
//...
53+
}
54+
}
55+
```
56+
57+
#### Example 2 Multiple class in the file
58+
59+
Bad:
60+
61+
File name: **some_other_widget.dart**
62+
63+
```dart
64+
class _SomeOtherWidget extends StatelessWidget {
65+
@override
66+
Widget build(BuildContext context) {
67+
//...
68+
}
69+
}
70+
71+
class SomeWidget extends StatelessWidget {
72+
@override
73+
Widget build(BuildContext context) {
74+
//...
75+
}
76+
}
77+
```
78+
79+
Good:
80+
81+
File name: **some_widget.dart**
82+
83+
```dart
84+
class _SomeOtherWidget extends StatelessWidget {
85+
@override
86+
Widget build(BuildContext context) {
87+
//...
88+
}
89+
}
90+
91+
class SomeWidget extends StatelessWidget {
92+
@override
93+
Widget build(BuildContext context) {
94+
//...
95+
}
96+
}
97+
```

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'rules_list/no_object_declaration/no_object_declaration.dart';
2222
import 'rules_list/prefer_conditional_expressions/prefer_conditional_expressions.dart';
2323
import 'rules_list/prefer_extracting_callbacks/prefer_extracting_callbacks.dart';
2424
import 'rules_list/prefer_intl_name/prefer_intl_name.dart';
25+
import 'rules_list/prefer_match_file_name/prefer_match_file_name.dart';
2526
import 'rules_list/prefer_on_push_cd_strategy/prefer_on_push_cd_strategy.dart';
2627
import 'rules_list/prefer_single_widget_per_file/prefer_single_widget_per_file.dart';
2728
import 'rules_list/prefer_trailing_comma/prefer_trailing_comma.dart';
@@ -63,6 +64,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
6364
PreferExtractingCallbacksRule.ruleId: (config) =>
6465
PreferExtractingCallbacksRule(config),
6566
PreferIntlNameRule.ruleId: (config) => PreferIntlNameRule(config),
67+
PreferMatchFileName.ruleId: (config) => PreferMatchFileName(config),
6668
PreferOnPushCdStrategyRule.ruleId: (config) =>
6769
PreferOnPushCdStrategyRule(config),
6870
PreferSingleWidgetPerFileRule.ruleId: (config) =>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/visitor.dart';
3+
import 'package:path/path.dart';
4+
5+
import '../../../../../utils/node_utils.dart';
6+
import '../../../models/internal_resolved_unit_result.dart';
7+
import '../../../models/issue.dart';
8+
import '../../../models/severity.dart';
9+
import '../../models/rule.dart';
10+
import '../../models/rule_documentation.dart';
11+
import '../../rule_utils.dart';
12+
13+
part 'visitor.dart';
14+
15+
class PreferMatchFileName extends Rule {
16+
static const String ruleId = 'prefer-match-file-name';
17+
static const _notMatchNameFailure =
18+
'File name does not match with first class name';
19+
static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]');
20+
21+
PreferMatchFileName([Map<String, Object> config = const {}])
22+
: super(
23+
id: ruleId,
24+
documentation: const RuleDocumentation(
25+
name: 'Prefer match file name',
26+
brief: 'Warns when file name does not match class name.',
27+
),
28+
severity: readSeverity(config, Severity.warning),
29+
excludes: readExcludes(config),
30+
);
31+
32+
bool _hasMatchName(String path, String className) {
33+
final classNameFormatted =
34+
className.replaceAll(_onlySymbolsRegex, '').toLowerCase();
35+
36+
return classNameFormatted ==
37+
basenameWithoutExtension(path)
38+
.replaceAll(_onlySymbolsRegex, '')
39+
.toLowerCase();
40+
}
41+
42+
@override
43+
Iterable<Issue> check(InternalResolvedUnitResult source) {
44+
final visitor = _Visitor();
45+
source.unit.visitChildren(visitor);
46+
47+
final _issue = <Issue>[];
48+
49+
if (visitor.declaration.isNotEmpty &&
50+
!_hasMatchName(source.path, visitor.declaration.first.name)) {
51+
final issue = createIssue(
52+
rule: this,
53+
location: nodeLocation(
54+
node: visitor.declaration.first,
55+
source: source,
56+
withCommentOrMetadata: true,
57+
),
58+
message: _notMatchNameFailure,
59+
);
60+
61+
_issue.add(issue);
62+
}
63+
64+
return _issue;
65+
}
66+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
part of 'prefer_match_file_name.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final _declarations = <SimpleIdentifier>[];
5+
6+
Iterable<SimpleIdentifier> get declaration =>
7+
_declarations..sort(_compareByPrivateType);
8+
9+
@override
10+
void visitClassDeclaration(ClassDeclaration node) {
11+
super.visitClassDeclaration(node);
12+
13+
_declarations.add(node.name);
14+
}
15+
16+
int _compareByPrivateType(SimpleIdentifier a, SimpleIdentifier b) {
17+
final isAPrivate = Identifier.isPrivateName(a.name);
18+
final isBPrivate = Identifier.isPrivateName(b.name);
19+
if (!isAPrivate && isBPrivate) {
20+
return -1;
21+
} else if (isAPrivate && !isBPrivate) {
22+
return 1;
23+
}
24+
25+
return a.offset.compareTo(b.offset);
26+
}
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class Example extends StatelessWidget {
2+
const Example({Key? key}) : super(key: key);
3+
4+
@override
5+
Widget build(BuildContext context) {
6+
return Container();
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class Example extends StatelessWidget {
2+
const Example({Key? key}) : super(key: key);
3+
4+
@override
5+
Widget build(BuildContext context) {
6+
return Container();
7+
}
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class ExampleWithState extends StatefulWidget {
2+
const ExampleWithState({Key? key}) : super(key: key);
3+
4+
@override
5+
_ExampleWithStateState createState() => _ExampleWithStateState();
6+
}
7+
8+
class _ExampleWithStateState extends State<ExampleWithState> {
9+
@override
10+
Widget build(BuildContext context) {
11+
return Container();
12+
}
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class _Multiclass {}
2+
3+
class MultipleClassesExample {}
4+
5+
class Test {}
6+
7+
class Absolut {}
8+
9+
class Best {}

0 commit comments

Comments
 (0)