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

Commit 7078881

Browse files
authored
feat: add avoid missing enum constant in map rule (#564)
* feat: add avoid missing enum constant in map rule * chore: add trailing comma
1 parent e96739f commit 7078881

File tree

8 files changed

+245
-1
lines changed

8 files changed

+245
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Unreleased
44

5-
* feat: add static code diagnostics `avoid-throw-in-catch-block`, `avoid-unnecessary-type-assertions`, `avoid-unnecessary-type-casts`
5+
* feat: add static code diagnostics `avoid-throw-in-catch-block`, `avoid-unnecessary-type-assertions`, `avoid-unnecessary-type-casts`,
6+
`avoid-missing-enum-constant-in-map`
67
* feat: introduce file metrics
78
* chore: activate self implemented rules: `avoid-unnecessary-type-assertions`, `avoid-unnecessary-type-casts`, `prefer-first`, `prefer-last`, `prefer-match-file-name`
89
* refactor: cleanup anti-patterns, metrics and rules documentation

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'rules_list/always_remove_listener/always_remove_listener_rule.dart';
33
import 'rules_list/avoid-unnecessary-type-casts/avoid_unnecessary_type_casts_rule.dart';
44
import 'rules_list/avoid_ignoring_return_values/avoid_ignoring_return_values_rule.dart';
55
import 'rules_list/avoid_late_keyword/avoid_late_keyword_rule.dart';
6+
import 'rules_list/avoid_missing_enum_constant_in_map/avoid_missing_enum_constant_in_map_rule.dart';
67
import 'rules_list/avoid_nested_conditional_expressions/avoid_nested_conditional_expressions_rule.dart';
78
import 'rules_list/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart';
89
import 'rules_list/avoid_preserve_whitespace_false/avoid_preserve_whitespace_false_rule.dart';
@@ -45,6 +46,8 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
4546
AvoidIgnoringReturnValuesRule.ruleId: (config) =>
4647
AvoidIgnoringReturnValuesRule(config),
4748
AvoidLateKeywordRule.ruleId: (config) => AvoidLateKeywordRule(config),
49+
AvoidMissingEnumConstantInMapRule.ruleId: (config) =>
50+
AvoidMissingEnumConstantInMapRule(config),
4851
AvoidNestedConditionalExpressionsRule.ruleId: (config) =>
4952
AvoidNestedConditionalExpressionsRule(config),
5053
AvoidNonNullAssertionRule.ruleId: (config) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// ignore_for_file: public_member_api_docs
2+
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer/dart/ast/visitor.dart';
5+
import 'package:analyzer/dart/element/element.dart';
6+
7+
import '../../../../../utils/node_utils.dart';
8+
import '../../../lint_utils.dart';
9+
import '../../../models/internal_resolved_unit_result.dart';
10+
import '../../../models/issue.dart';
11+
import '../../../models/severity.dart';
12+
import '../../models/common_rule.dart';
13+
import '../../rule_utils.dart';
14+
15+
part 'visitor.dart';
16+
17+
class AvoidMissingEnumConstantInMapRule extends CommonRule {
18+
static const String ruleId = 'avoid-missing-enum-constant-in-map';
19+
20+
static const _warning = 'Missing map entry for';
21+
22+
AvoidMissingEnumConstantInMapRule([Map<String, Object> config = const {}])
23+
: super(
24+
id: ruleId,
25+
severity: readSeverity(config, Severity.warning),
26+
excludes: readExcludes(config),
27+
);
28+
29+
@override
30+
Iterable<Issue> check(InternalResolvedUnitResult source) {
31+
final visitor = _Visitor();
32+
33+
source.unit.visitChildren(visitor);
34+
35+
return visitor.declarations
36+
.map((declaration) => createIssue(
37+
rule: this,
38+
location: nodeLocation(
39+
node: declaration.parent,
40+
source: source,
41+
),
42+
message: '$_warning ${declaration.name}',
43+
))
44+
.toList(growable: false);
45+
}
46+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
part of 'avoid_missing_enum_constant_in_map_rule.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final _declarations = <_MapInfo>[];
5+
6+
Iterable<_MapInfo> get declarations => _declarations;
7+
8+
@override
9+
void visitSetOrMapLiteral(SetOrMapLiteral node) {
10+
super.visitSetOrMapLiteral(node);
11+
12+
if (!node.isMap) {
13+
return;
14+
}
15+
16+
final usages = <String>[];
17+
ClassElement? enumElement;
18+
19+
for (final element in node.elements) {
20+
if (element is MapLiteralEntry) {
21+
final key = element.key;
22+
if (key is PrefixedIdentifier) {
23+
final staticElement = key.prefix.staticElement;
24+
if (staticElement is ClassElement &&
25+
staticElement.kind == ElementKind.ENUM) {
26+
enumElement ??= staticElement;
27+
usages.add(key.identifier.name);
28+
}
29+
}
30+
}
31+
}
32+
33+
final parent = node.parent;
34+
final fields = enumElement?.fields;
35+
if (fields != null && parent != null) {
36+
for (final field in fields) {
37+
final name = field.displayName;
38+
if (field.isConst && !usages.contains(name) && name != 'values') {
39+
_declarations.add(_MapInfo(name, parent));
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
class _MapInfo {
47+
final AstNode parent;
48+
final String name;
49+
50+
const _MapInfo(this.name, this.parent);
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@TestOn('vm')
2+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
3+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/avoid_missing_enum_constant_in_map/avoid_missing_enum_constant_in_map_rule.dart';
4+
import 'package:test/test.dart';
5+
6+
import '../../../../../helpers/rule_test_helper.dart';
7+
8+
const _examplePath = 'avoid_missing_enum_constant_in_map/examples/example.dart';
9+
10+
void main() {
11+
group('AvoidMissingEnumConstantInMapRule', () {
12+
test('initialization', () async {
13+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
14+
final issues = AvoidMissingEnumConstantInMapRule().check(unit);
15+
16+
RuleTestHelper.verifyInitialization(
17+
issues: issues,
18+
ruleId: 'avoid-missing-enum-constant-in-map',
19+
severity: Severity.warning,
20+
);
21+
});
22+
23+
test('reports about found issues with the default config', () async {
24+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
25+
final issues = AvoidMissingEnumConstantInMapRule().check(unit);
26+
27+
RuleTestHelper.verifyIssues(
28+
issues: issues,
29+
startOffsets: [119, 241, 241],
30+
startLines: [9, 15, 15],
31+
startColumns: [16, 16, 16],
32+
endOffsets: [213, 308, 308],
33+
locationTexts: [
34+
'_code = <CountyCode, String>{\n'
35+
" CountyCode.russia: 'RUS',\n"
36+
" CountyCode.another: 'XXX',\n"
37+
' }',
38+
'_title = <CountyCode, String>{\n'
39+
" CountyCode.russia: 'Россия',\n"
40+
' }',
41+
'_title = <CountyCode, String>{\n'
42+
" CountyCode.russia: 'Россия',\n"
43+
' }',
44+
],
45+
messages: [
46+
'Missing map entry for kazachstan',
47+
'Missing map entry for kazachstan',
48+
'Missing map entry for another',
49+
],
50+
);
51+
});
52+
});
53+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
enum CountyCode {
2+
russia,
3+
kazachstan,
4+
another,
5+
}
6+
7+
extension CountyTypeX on CountyCode {
8+
// LINT
9+
static const _code = <CountyCode, String>{
10+
CountyCode.russia: 'RUS',
11+
CountyCode.another: 'XXX',
12+
};
13+
14+
// LINT
15+
static const _title = <CountyCode, String>{
16+
CountyCode.russia: 'Россия',
17+
};
18+
19+
static CountyCode? fromCode(String? code) => enumDecodeNullable(_code, code);
20+
21+
String get title => _title[this]!;
22+
23+
String get code => _code[this]!;
24+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Avoid late keyword
2+
3+
## Rule id {#rule-id}
4+
5+
avoid-missing-enum-constant-in-map
6+
7+
## Severity {#severity}
8+
9+
Warning
10+
11+
## Description {#description}
12+
13+
Warns when a enum constant is missing in a map declaration.
14+
15+
### Example {#example}
16+
17+
Bad:
18+
19+
```dart
20+
enum SomeEnum {
21+
firstEntry,
22+
secondEntry,
23+
thirdEntry,
24+
}
25+
26+
extension SomeX on SomeEnum {
27+
// LINT
28+
static const firstMap = <SomeEnum, String>{
29+
CountyCode.firstEntry: 'foo',
30+
CountyCode.secondEntry: 'bar',
31+
};
32+
33+
// LINT twice since `secondEntry` and `thirdEntry` are missing
34+
static const secondMap = <SomeEnum, String>{
35+
CountyCode.firstEntry: 'foo',
36+
};
37+
}
38+
```
39+
40+
Good:
41+
42+
```dart
43+
enum SomeEnum {
44+
firstEntry,
45+
secondEntry,
46+
thirdEntry,
47+
}
48+
49+
extension SomeX on SomeEnum {
50+
static const firstMap = <SomeEnum, String>{
51+
CountyCode.firstEntry: 'foo',
52+
CountyCode.secondEntry: 'bar',
53+
CountyCode.thirdEntry: 'baz',
54+
};
55+
56+
static const secondMap = <SomeEnum, String>{
57+
CountyCode.firstEntry: 'foo',
58+
CountyCode.secondEntry: 'bar',
59+
CountyCode.thirdEntry: 'baz',
60+
};
61+
}
62+
```

website/docs/rules/overview.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Rules configuration is [described here](../getting-started/configuration#configu
1919

2020
Warns when a field or variable is declared with a `late` keyword.
2121

22+
- [avoid-missing-enum-constant-in-map](./common/avoid-missing-enum-constant-in-map.md)
23+
24+
Warns when a enum constant is missing in a map declaration.
25+
2226
- [avoid-nested-conditional-expressions](./common/avoid-nested-conditional-expressions.md) &nbsp; [![Configurable](https://img.shields.io/badge/-configurable-informational)](./common/member-ordering.md#config-example)
2327

2428
Warns about nested conditional expressions.

0 commit comments

Comments
 (0)