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

Commit f525dda

Browse files
fzyzcjyincendialdkrutskikh
committed
feat: create tag-name; create ban-name (#722)
* add doc * add empty ban_name_rule * add tests * config parser * implement rule (without visitor) * try to implement visitor * fix bugs * enhance tests * fix wrong ruleid * change severity * rephrase * changelog * add more tests * scaffold for tag-name * add tests * partial impl * more implementations * pass tests * enhance tests and still pass tests * format code * update changelog * fix linter issue * regsiter TagNameRule * add stripPrefix, stripPostfix * explain limitation Co-authored-by: Dmitry Zhifarsky <dimannich2380@gmail.com> Co-authored-by: Dmitry Krutskikh <dmitry.krutskikh@gmail.com>
1 parent 33c0af4 commit f525dda

File tree

17 files changed

+538
-1
lines changed

17 files changed

+538
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# JetBrains IDEs
44
.idea/
5+
*.iml
56

67
# Files and directories created by pub
78
.dart_tool/

CHANGELOG.md

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

33
## Unreleased
44

5-
* feat: add static code diagnostic `avoid-collection-methods-with-unrelated-types`
5+
* feat: add static code diagnostics `avoid-collection-methods-with-unrelated-types`, `ban-name`, `tag-name`.
66
* fix: added parameter constant check in `avoid-border-all`.
77
* chore: set min `mocktail` version to `^0.3.0`.
88

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'rules_list/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rul
1818
import 'rules_list/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart';
1919
import 'rules_list/avoid_unused_parameters/avoid_unused_parameters_rule.dart';
2020
import 'rules_list/avoid_wrapping_in_padding/avoid_wrapping_in_padding_rule.dart';
21+
import 'rules_list/ban_name/ban_name_rule.dart';
2122
import 'rules_list/binary_expression_operand_order/binary_expression_operand_order_rule.dart';
2223
import 'rules_list/component_annotation_arguments_ordering/component_annotation_arguments_ordering_rule.dart';
2324
import 'rules_list/double_literal_format/double_literal_format_rule.dart';
@@ -45,6 +46,7 @@ import 'rules_list/prefer_on_push_cd_strategy/prefer_on_push_cd_strategy_rule.da
4546
import 'rules_list/prefer_single_widget_per_file/prefer_single_widget_per_file_rule.dart';
4647
import 'rules_list/prefer_trailing_comma/prefer_trailing_comma_rule.dart';
4748
import 'rules_list/provide_correct_intl_args/provide_correct_intl_args_rule.dart';
49+
import 'rules_list/tag_name/tag_name_rule.dart';
4850

4951
final _implementedRules = <String, Rule Function(Map<String, Object>)>{
5052
AlwaysRemoveListenerRule.ruleId: (config) => AlwaysRemoveListenerRule(config),
@@ -80,6 +82,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
8082
AvoidUnusedParametersRule(config),
8183
AvoidWrappingInPaddingRule.ruleId: (config) =>
8284
AvoidWrappingInPaddingRule(config),
85+
BanNameRule.ruleId: (config) => BanNameRule(config),
8386
BinaryExpressionOperandOrderRule.ruleId: (config) =>
8487
BinaryExpressionOperandOrderRule(config),
8588
ComponentAnnotationArgumentsOrderingRule.ruleId: (config) =>
@@ -119,6 +122,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
119122
PreferTrailingCommaRule.ruleId: (config) => PreferTrailingCommaRule(config),
120123
ProvideCorrectIntlArgsRule.ruleId: (config) =>
121124
ProvideCorrectIntlArgsRule(config),
125+
TagNameRule.ruleId: (config) => TagNameRule(config),
122126
};
123127

124128
Iterable<Rule> get allRules =>
Lines changed: 46 additions & 0 deletions
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+
6+
import '../../../../../utils/node_utils.dart';
7+
import '../../../lint_utils.dart';
8+
import '../../../models/internal_resolved_unit_result.dart';
9+
import '../../../models/issue.dart';
10+
import '../../../models/severity.dart';
11+
import '../../models/common_rule.dart';
12+
import '../../rule_utils.dart';
13+
14+
part 'visitor.dart';
15+
part 'utils/config_parser.dart';
16+
17+
class BanNameRule extends CommonRule {
18+
static const String ruleId = 'ban-name';
19+
20+
final List<_BanNameConfigEntry> _entries;
21+
22+
BanNameRule([Map<String, Object> config = const {}])
23+
: _entries = _ConfigParser._parseEntryConfig(config),
24+
super(
25+
id: ruleId,
26+
severity: readSeverity(config, Severity.style),
27+
excludes: readExcludes(config),
28+
);
29+
30+
@override
31+
Iterable<Issue> check(InternalResolvedUnitResult source) {
32+
final visitor = _Visitor(_entries);
33+
34+
source.unit.visitChildren(visitor);
35+
36+
return visitor.nodes
37+
.map(
38+
(node) => createIssue(
39+
rule: this,
40+
location: nodeLocation(node: node.node, source: source),
41+
message: node.message,
42+
),
43+
)
44+
.toList(growable: false);
45+
}
46+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
part of '../ban_name_rule.dart';
2+
3+
const _entriesLabel = 'entries';
4+
const _identLabel = 'ident';
5+
const _descriptionLabel = 'description';
6+
7+
/// Parser for rule configuration.
8+
class _ConfigParser {
9+
static List<_BanNameConfigEntry> _parseEntryConfig(
10+
Map<String, Object> config,
11+
) =>
12+
(config[_entriesLabel] as Iterable<Object?>? ?? []).map((entry) {
13+
final entryMap = entry as Map<Object?, Object?>;
14+
15+
return _BanNameConfigEntry(
16+
ident: entryMap[_identLabel] as String,
17+
description: entryMap[_descriptionLabel] as String,
18+
);
19+
}).toList();
20+
}
21+
22+
class _BanNameConfigEntry {
23+
final String ident;
24+
final String description;
25+
26+
_BanNameConfigEntry({required this.ident, required this.description});
27+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
part of 'ban_name_rule.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final Map<String, _BanNameConfigEntry> _entryMap;
5+
6+
final _nodes = <_NodeWithMessage>[];
7+
8+
Iterable<_NodeWithMessage> get nodes => _nodes;
9+
10+
_Visitor(List<_BanNameConfigEntry> entries)
11+
: _entryMap = Map.fromEntries(entries.map((e) => MapEntry(e.ident, e)));
12+
13+
@override
14+
void visitSimpleIdentifier(SimpleIdentifier node) {
15+
super.visitSimpleIdentifier(node);
16+
_visitIdent(node, node);
17+
}
18+
19+
@override
20+
void visitPrefixedIdentifier(PrefixedIdentifier node) {
21+
super.visitPrefixedIdentifier(node);
22+
_visitIdent(node, node.identifier);
23+
_visitIdent(node, node.prefix);
24+
}
25+
26+
@override
27+
void visitLibraryIdentifier(LibraryIdentifier node) {
28+
super.visitLibraryIdentifier(node);
29+
for (final component in node.components) {
30+
_visitIdent(node, component);
31+
}
32+
}
33+
34+
void _visitIdent(Expression node, SimpleIdentifier ident) {
35+
final name = ident.name;
36+
if (_entryMap.containsKey(name)) {
37+
_nodes.add(_NodeWithMessage(
38+
node,
39+
'${_entryMap[name]!.description} ($name is banned)',
40+
));
41+
}
42+
}
43+
}
44+
45+
class _NodeWithMessage {
46+
final Expression node;
47+
final String message;
48+
49+
_NodeWithMessage(this.node, this.message);
50+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
6+
import '../../../../../utils/node_utils.dart';
7+
import '../../../lint_utils.dart';
8+
import '../../../models/internal_resolved_unit_result.dart';
9+
import '../../../models/issue.dart';
10+
import '../../../models/replacement.dart';
11+
import '../../../models/severity.dart';
12+
import '../../models/common_rule.dart';
13+
import '../../rule_utils.dart';
14+
15+
part 'visitor.dart';
16+
part 'utils/config_parser.dart';
17+
18+
class TagNameRule extends CommonRule {
19+
static const String ruleId = 'tag-name';
20+
21+
static const _warning = 'Tag name should match class name';
22+
23+
final _ParsedConfig _parsedConfig;
24+
25+
TagNameRule([Map<String, Object> config = const {}])
26+
: _parsedConfig = _ConfigParser._parseConfig(config),
27+
super(
28+
id: ruleId,
29+
severity: readSeverity(config, Severity.warning),
30+
excludes: readExcludes(config),
31+
);
32+
33+
@override
34+
Iterable<Issue> check(InternalResolvedUnitResult source) {
35+
final visitor = _Visitor(_parsedConfig);
36+
37+
source.unit.visitChildren(visitor);
38+
39+
return visitor.nodes
40+
.map((node) => createIssue(
41+
rule: this,
42+
location: nodeLocation(
43+
node: node.initializer,
44+
source: source,
45+
),
46+
message: _warning,
47+
replacement: node.replacement,
48+
))
49+
.toList(growable: false);
50+
}
51+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
part of '../tag_name_rule.dart';
2+
3+
const _varNamesLabel = 'var-names';
4+
const _stripPrefixLabel = 'strip-prefix';
5+
const _stripPostfixLabel = 'strip-postfix';
6+
7+
/// Parser for rule configuration.
8+
class _ConfigParser {
9+
static _ParsedConfig _parseConfig(Map<String, Object> value) => _ParsedConfig(
10+
varNames: (value[_varNamesLabel] as List<Object>? ?? <Object>[])
11+
.map((item) => item as String)
12+
.toList(),
13+
stripPrefix: value[_stripPrefixLabel] as String? ?? '',
14+
stripPostfix: value[_stripPostfixLabel] as String? ?? '',
15+
);
16+
}
17+
18+
class _ParsedConfig {
19+
final List<String> varNames;
20+
final String stripPrefix;
21+
final String stripPostfix;
22+
23+
_ParsedConfig({
24+
required this.varNames,
25+
required this.stripPrefix,
26+
required this.stripPostfix,
27+
});
28+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
part of 'tag_name_rule.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final _ParsedConfig _parsedConfig;
5+
6+
_Visitor(this._parsedConfig);
7+
8+
final _nodes = <_NodeWithMessage>[];
9+
10+
Iterable<_NodeWithMessage> get nodes => _nodes;
11+
12+
@override
13+
void visitFieldDeclaration(FieldDeclaration node) {
14+
super.visitFieldDeclaration(node);
15+
16+
for (final fieldVariable in node.fields.variables) {
17+
_checkField(node, fieldVariable);
18+
}
19+
}
20+
21+
void _checkField(FieldDeclaration node, VariableDeclaration fieldVariable) {
22+
final fieldName = fieldVariable.name.name;
23+
final fieldType = fieldVariable.declaredElement?.type;
24+
25+
if (!(fieldType != null &&
26+
fieldType.isDartCoreString &&
27+
_parsedConfig.varNames.contains(fieldName))) {
28+
return;
29+
}
30+
31+
final fieldInitializer = fieldVariable.initializer;
32+
if (fieldInitializer is! StringLiteral) {
33+
return;
34+
}
35+
final fieldInitValue = fieldInitializer.stringValue;
36+
37+
final classDeclaration = node.thisOrAncestorOfType<ClassDeclaration>();
38+
final className = classDeclaration?.name.name;
39+
if (className == null) {
40+
return;
41+
}
42+
final expectFieldValue = _calculateExpectFieldValue(className);
43+
44+
if (fieldInitValue != expectFieldValue) {
45+
_nodes.add(_NodeWithMessage(
46+
fieldInitializer,
47+
Replacement(
48+
comment: "Replace with '$expectFieldValue'",
49+
replacement: "'$expectFieldValue'",
50+
),
51+
));
52+
}
53+
}
54+
55+
String _calculateExpectFieldValue(String className) {
56+
var ans = className;
57+
if (className.startsWith(_parsedConfig.stripPrefix)) {
58+
ans = ans.substring(_parsedConfig.stripPrefix.length);
59+
}
60+
if (className.endsWith(_parsedConfig.stripPostfix)) {
61+
ans = ans.substring(0, ans.length - _parsedConfig.stripPostfix.length);
62+
}
63+
64+
return ans;
65+
}
66+
}
67+
68+
class _NodeWithMessage {
69+
final StringLiteral initializer;
70+
final Replacement replacement;
71+
72+
_NodeWithMessage(this.initializer, this.replacement);
73+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
2+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/ban_name/ban_name_rule.dart';
3+
import 'package:test/test.dart';
4+
5+
import '../../../../../helpers/rule_test_helper.dart';
6+
7+
const _examplePath = 'ban_name/examples/example.dart';
8+
9+
void main() {
10+
group('BanNameRule', () {
11+
test('initialization', () async {
12+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
13+
final issues = BanNameRule().check(unit);
14+
15+
RuleTestHelper.verifyInitialization(
16+
issues: issues,
17+
ruleId: 'ban-name',
18+
severity: Severity.style,
19+
);
20+
});
21+
22+
test('reports about all found issues in example.dart', () async {
23+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
24+
25+
final issues = BanNameRule({
26+
'entries': [
27+
{'ident': 'showDialog', 'description': 'Please use myShowDialog'},
28+
{'ident': 'strangeName', 'description': 'The name is too strange'},
29+
{'ident': 'AnotherStrangeName', 'description': 'Oops'},
30+
],
31+
}).check(unit);
32+
33+
RuleTestHelper.verifyIssues(
34+
issues: issues,
35+
startLines: [6, 7, 9, 12, 15, 16],
36+
startColumns: [3, 12, 7, 6, 7, 12],
37+
locationTexts: [
38+
'showDialog',
39+
'showDialog',
40+
'strangeName',
41+
'strangeName',
42+
'AnotherStrangeName',
43+
'strangeName',
44+
],
45+
messages: [
46+
'Please use myShowDialog (showDialog is banned)',
47+
'Please use myShowDialog (showDialog is banned)',
48+
'The name is too strange (strangeName is banned)',
49+
'The name is too strange (strangeName is banned)',
50+
'Oops (AnotherStrangeName is banned)',
51+
'The name is too strange (strangeName is banned)',
52+
],
53+
);
54+
});
55+
});
56+
}

0 commit comments

Comments
 (0)