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

Commit 29e6181

Browse files
authored
feat: introduce prefer-provide-intl-description rule (#1137)
* feat: introduce prefer-provide-intl-description rule * chore: fixes after review
1 parent 592cd3d commit 29e6181

File tree

9 files changed

+463
-0
lines changed

9 files changed

+463
-0
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
* feat: add static code diagnostic [`prefer-provide-intl-description`](https://dartcodemetrics.dev/docs/rules/intl/prefer-provide-intl-description).
56
* feat: exclude `.freezed.dart` files by default.
67
* fix: handle try and switch statements for [`use-setstate-synchronously`](https://dartcodemetrics.dev/docs/rules/flutter/use-setstate-synchronously)
78
* chore: restrict `analyzer` version to `>=5.1.0 <5.4.0`.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import 'rules_list/prefer_last/prefer_last_rule.dart';
6565
import 'rules_list/prefer_match_file_name/prefer_match_file_name_rule.dart';
6666
import 'rules_list/prefer_moving_to_variable/prefer_moving_to_variable_rule.dart';
6767
import 'rules_list/prefer_on_push_cd_strategy/prefer_on_push_cd_strategy_rule.dart';
68+
import 'rules_list/prefer_provide_intl_description/prefer_provide_intl_description_rule.dart';
6869
import 'rules_list/prefer_single_widget_per_file/prefer_single_widget_per_file_rule.dart';
6970
import 'rules_list/prefer_static_class/prefer_static_class_rule.dart';
7071
import 'rules_list/prefer_trailing_comma/prefer_trailing_comma_rule.dart';
@@ -150,6 +151,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
150151
PreferMatchFileNameRule.ruleId: PreferMatchFileNameRule.new,
151152
PreferMovingToVariableRule.ruleId: PreferMovingToVariableRule.new,
152153
PreferOnPushCdStrategyRule.ruleId: PreferOnPushCdStrategyRule.new,
154+
PreferProvideIntlDescriptionRule.ruleId: PreferProvideIntlDescriptionRule.new,
153155
PreferSingleWidgetPerFileRule.ruleId: PreferSingleWidgetPerFileRule.new,
154156
PreferStaticClassRule.ruleId: PreferStaticClassRule.new,
155157
PreferTrailingCommaRule.ruleId: PreferTrailingCommaRule.new,
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/intl_rule.dart';
12+
import '../../rule_utils.dart';
13+
14+
part 'visitor.dart';
15+
16+
class PreferProvideIntlDescriptionRule extends IntlRule {
17+
static const String ruleId = 'prefer-provide-intl-description';
18+
19+
static const _warning = 'Provide description for translated message';
20+
21+
PreferProvideIntlDescriptionRule([Map<String, Object> config = const {}])
22+
: super(
23+
id: ruleId,
24+
severity: readSeverity(config, Severity.warning),
25+
excludes: readExcludes(config),
26+
includes: readIncludes(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,
40+
source: source,
41+
),
42+
message: _warning,
43+
))
44+
.toList(growable: false);
45+
}
46+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
part of 'prefer_provide_intl_description_rule.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
static const _supportedMethods = {'message', 'plural', 'gender', 'select'};
5+
6+
final _declarations = <MethodInvocation>[];
7+
8+
Iterable<MethodInvocation> get declarations => _declarations;
9+
10+
@override
11+
void visitMethodInvocation(MethodInvocation node) {
12+
super.visitMethodInvocation(node);
13+
14+
final target = node.realTarget;
15+
if (target != null &&
16+
target is SimpleIdentifier &&
17+
target.name == 'Intl' &&
18+
_supportedMethods.contains(node.methodName.name) &&
19+
_withEmptyDescription(node.argumentList)) {
20+
_declarations.add(node);
21+
}
22+
}
23+
24+
bool _withEmptyDescription(ArgumentList args) =>
25+
args.arguments.any((argument) =>
26+
argument is NamedExpression &&
27+
argument.name.label.name == 'desc' &&
28+
argument.expression is SimpleStringLiteral &&
29+
(argument.expression as SimpleStringLiteral).value.isEmpty) ||
30+
args.arguments.every((argument) =>
31+
argument is! NamedExpression || argument.name.label.name != 'desc');
32+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
class SomeClassI18n {
2+
static final String message = Intl.message(
3+
'message',
4+
name: 'SomeClassI18n_message',
5+
desc: 'Message description',
6+
);
7+
8+
static String plural = Intl.plural(
9+
1,
10+
one: 'one',
11+
other: 'other',
12+
name: 'SomeClassI18n_plural',
13+
desc: 'Plural description',
14+
);
15+
16+
static String gender = Intl.gender(
17+
'other',
18+
female: 'female',
19+
male: 'male',
20+
other: 'other',
21+
name: 'SomeClassI18n_gender',
22+
desc: 'Gender description',
23+
);
24+
25+
static String select = Intl.select(
26+
true,
27+
{true: 'true', false: 'false'},
28+
name: 'SomeClassI18n_select',
29+
desc: 'Select description',
30+
);
31+
}
32+
33+
class Intl {
34+
static String message(String messageText,
35+
{String? desc = '',
36+
Map<String, Object>? examples,
37+
String? locale,
38+
String? name,
39+
List<Object>? args,
40+
String? meaning,
41+
bool? skip}) =>
42+
'';
43+
44+
static String plural(num howMany,
45+
{String? zero,
46+
String? one,
47+
String? two,
48+
String? few,
49+
String? many,
50+
required String other,
51+
String? desc,
52+
Map<String, Object>? examples,
53+
String? locale,
54+
int? precision,
55+
String? name,
56+
List<Object>? args,
57+
String? meaning,
58+
bool? skip}) =>
59+
'';
60+
61+
static String gender(String targetGender,
62+
{String? female,
63+
String? male,
64+
required String other,
65+
String? desc,
66+
Map<String, Object>? examples,
67+
String? locale,
68+
String? name,
69+
List<Object>? args,
70+
String? meaning,
71+
bool? skip}) =>
72+
'';
73+
74+
static String select(Object choice, Map<Object, String> cases,
75+
{String? desc,
76+
Map<String, Object>? examples,
77+
String? locale,
78+
String? name,
79+
List<Object>? args,
80+
String? meaning,
81+
bool? skip}) =>
82+
'';
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
class SomeClassI18n {
2+
static final String message = Intl.message(
3+
'message',
4+
name: 'SomeClassI18n_message',
5+
desc: '',
6+
);
7+
8+
static final String message2 = Intl.message(
9+
'message2',
10+
name: 'SomeClassI18n_message2',
11+
);
12+
13+
static String plural = Intl.plural(
14+
1,
15+
one: 'one',
16+
other: 'other',
17+
name: 'SomeClassI18n_plural',
18+
desc: '',
19+
);
20+
21+
static String plural2 = Intl.plural(
22+
2,
23+
one: 'one',
24+
other: 'other',
25+
name: 'SomeClassI18n_plural2',
26+
);
27+
28+
static String gender = Intl.gender(
29+
'other',
30+
female: 'female',
31+
male: 'male',
32+
other: 'other',
33+
name: 'SomeClassI18n_gender',
34+
desc: '',
35+
);
36+
37+
static String gender2 = Intl.gender(
38+
'other',
39+
female: 'female',
40+
male: 'male',
41+
other: 'other',
42+
name: 'SomeClassI18n_gender2',
43+
);
44+
45+
static String select = Intl.select(
46+
true,
47+
{true: 'true', false: 'false'},
48+
name: 'SomeClassI18n_select',
49+
desc: '',
50+
);
51+
52+
static String select2 = Intl.select(
53+
false,
54+
{true: 'true', false: 'false'},
55+
name: 'SomeClassI18n_select',
56+
);
57+
}
58+
59+
class Intl {
60+
Intl();
61+
62+
static String message(String messageText,
63+
{String? desc = '',
64+
Map<String, Object>? examples,
65+
String? locale,
66+
String? name,
67+
List<Object>? args,
68+
String? meaning,
69+
bool? skip}) =>
70+
'';
71+
72+
static String plural(num howMany,
73+
{String? zero,
74+
String? one,
75+
String? two,
76+
String? few,
77+
String? many,
78+
required String other,
79+
String? desc,
80+
Map<String, Object>? examples,
81+
String? locale,
82+
int? precision,
83+
String? name,
84+
List<Object>? args,
85+
String? meaning,
86+
bool? skip}) =>
87+
'';
88+
89+
static String gender(String targetGender,
90+
{String? female,
91+
String? male,
92+
required String other,
93+
String? desc,
94+
Map<String, Object>? examples,
95+
String? locale,
96+
String? name,
97+
List<Object>? args,
98+
String? meaning,
99+
bool? skip}) =>
100+
'';
101+
102+
static String select(Object choice, Map<Object, String> cases,
103+
{String? desc,
104+
Map<String, Object>? examples,
105+
String? locale,
106+
String? name,
107+
List<Object>? args,
108+
String? meaning,
109+
bool? skip}) =>
110+
'';
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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/prefer_provide_intl_description/prefer_provide_intl_description_rule.dart';
3+
import 'package:test/test.dart';
4+
5+
import '../../../../../helpers/rule_test_helper.dart';
6+
7+
const _examplePath = 'prefer_provide_intl_description/examples/example.dart';
8+
const _incorrectExamplePath =
9+
'prefer_provide_intl_description/examples/incorrect_example.dart';
10+
11+
void main() {
12+
group('$PreferProvideIntlDescriptionRule', () {
13+
test('initialization', () async {
14+
final unit = await RuleTestHelper.resolveFromFile(_incorrectExamplePath);
15+
final issues = PreferProvideIntlDescriptionRule().check(unit);
16+
17+
RuleTestHelper.verifyInitialization(
18+
issues: issues,
19+
ruleId: 'prefer-provide-intl-description',
20+
severity: Severity.warning,
21+
);
22+
});
23+
24+
test('reports no issues', () async {
25+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
26+
final issues = PreferProvideIntlDescriptionRule().check(unit);
27+
28+
RuleTestHelper.verifyNoIssues(issues);
29+
});
30+
31+
test('reports about found issues for incorrect names', () async {
32+
final unit = await RuleTestHelper.resolveFromFile(_incorrectExamplePath);
33+
final issues = PreferProvideIntlDescriptionRule().check(unit);
34+
35+
RuleTestHelper.verifyIssues(
36+
issues: issues,
37+
startLines: [2, 8, 13, 21, 28, 37, 45, 52],
38+
startColumns: [33, 34, 26, 27, 26, 27, 26, 27],
39+
locationTexts: [
40+
'Intl.message(\n'
41+
" 'message',\n"
42+
" name: 'SomeClassI18n_message',\n"
43+
" desc: '',\n"
44+
' )',
45+
'Intl.message(\n'
46+
" 'message2',\n"
47+
" name: 'SomeClassI18n_message2',\n"
48+
' )',
49+
'Intl.plural(\n'
50+
' 1,\n'
51+
" one: 'one',\n"
52+
" other: 'other',\n"
53+
" name: 'SomeClassI18n_plural',\n"
54+
" desc: '',\n"
55+
' )',
56+
'Intl.plural(\n'
57+
' 2,\n'
58+
" one: 'one',\n"
59+
" other: 'other',\n"
60+
" name: 'SomeClassI18n_plural2',\n"
61+
' )',
62+
'Intl.gender(\n'
63+
" 'other',\n"
64+
" female: 'female',\n"
65+
" male: 'male',\n"
66+
" other: 'other',\n"
67+
" name: 'SomeClassI18n_gender',\n"
68+
" desc: '',\n"
69+
' )',
70+
'Intl.gender(\n'
71+
" 'other',\n"
72+
" female: 'female',\n"
73+
" male: 'male',\n"
74+
" other: 'other',\n"
75+
" name: 'SomeClassI18n_gender2',\n"
76+
' )',
77+
'Intl.select(\n'
78+
' true,\n'
79+
" {true: 'true', false: 'false'},\n"
80+
" name: 'SomeClassI18n_select',\n"
81+
" desc: '',\n"
82+
' )',
83+
'Intl.select(\n'
84+
' false,\n'
85+
" {true: 'true', false: 'false'},\n"
86+
" name: 'SomeClassI18n_select',\n"
87+
' )',
88+
],
89+
messages: List.filled(
90+
issues.length,
91+
'Provide description for translated message',
92+
),
93+
);
94+
});
95+
});
96+
}

0 commit comments

Comments
 (0)