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

Commit

Permalink
feat: introduce prefer-provide-intl-description rule (#1137)
Browse files Browse the repository at this point in the history
* feat: introduce prefer-provide-intl-description rule

* chore: fixes after review
  • Loading branch information
dkrutskikh committed Jan 8, 2023
1 parent 592cd3d commit 29e6181
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

## Unreleased

* feat: add static code diagnostic [`prefer-provide-intl-description`](https://dartcodemetrics.dev/docs/rules/intl/prefer-provide-intl-description).
* feat: exclude `.freezed.dart` files by default.
* fix: handle try and switch statements for [`use-setstate-synchronously`](https://dartcodemetrics.dev/docs/rules/flutter/use-setstate-synchronously)
* chore: restrict `analyzer` version to `>=5.1.0 <5.4.0`.
Expand Down
2 changes: 2 additions & 0 deletions lib/src/analyzers/lint_analyzer/rules/rules_factory.dart
Expand Up @@ -65,6 +65,7 @@ import 'rules_list/prefer_last/prefer_last_rule.dart';
import 'rules_list/prefer_match_file_name/prefer_match_file_name_rule.dart';
import 'rules_list/prefer_moving_to_variable/prefer_moving_to_variable_rule.dart';
import 'rules_list/prefer_on_push_cd_strategy/prefer_on_push_cd_strategy_rule.dart';
import 'rules_list/prefer_provide_intl_description/prefer_provide_intl_description_rule.dart';
import 'rules_list/prefer_single_widget_per_file/prefer_single_widget_per_file_rule.dart';
import 'rules_list/prefer_static_class/prefer_static_class_rule.dart';
import 'rules_list/prefer_trailing_comma/prefer_trailing_comma_rule.dart';
Expand Down Expand Up @@ -150,6 +151,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
PreferMatchFileNameRule.ruleId: PreferMatchFileNameRule.new,
PreferMovingToVariableRule.ruleId: PreferMovingToVariableRule.new,
PreferOnPushCdStrategyRule.ruleId: PreferOnPushCdStrategyRule.new,
PreferProvideIntlDescriptionRule.ruleId: PreferProvideIntlDescriptionRule.new,
PreferSingleWidgetPerFileRule.ruleId: PreferSingleWidgetPerFileRule.new,
PreferStaticClassRule.ruleId: PreferStaticClassRule.new,
PreferTrailingCommaRule.ruleId: PreferTrailingCommaRule.new,
Expand Down
@@ -0,0 +1,46 @@
// ignore_for_file: public_member_api_docs

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';

import '../../../../../utils/node_utils.dart';
import '../../../lint_utils.dart';
import '../../../models/internal_resolved_unit_result.dart';
import '../../../models/issue.dart';
import '../../../models/severity.dart';
import '../../models/intl_rule.dart';
import '../../rule_utils.dart';

part 'visitor.dart';

class PreferProvideIntlDescriptionRule extends IntlRule {
static const String ruleId = 'prefer-provide-intl-description';

static const _warning = 'Provide description for translated message';

PreferProvideIntlDescriptionRule([Map<String, Object> config = const {}])
: super(
id: ruleId,
severity: readSeverity(config, Severity.warning),
excludes: readExcludes(config),
includes: readIncludes(config),
);

@override
Iterable<Issue> check(InternalResolvedUnitResult source) {
final visitor = _Visitor();

source.unit.visitChildren(visitor);

return visitor.declarations
.map((declaration) => createIssue(
rule: this,
location: nodeLocation(
node: declaration,
source: source,
),
message: _warning,
))
.toList(growable: false);
}
}
@@ -0,0 +1,32 @@
part of 'prefer_provide_intl_description_rule.dart';

class _Visitor extends RecursiveAstVisitor<void> {
static const _supportedMethods = {'message', 'plural', 'gender', 'select'};

final _declarations = <MethodInvocation>[];

Iterable<MethodInvocation> get declarations => _declarations;

@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);

final target = node.realTarget;
if (target != null &&
target is SimpleIdentifier &&
target.name == 'Intl' &&
_supportedMethods.contains(node.methodName.name) &&
_withEmptyDescription(node.argumentList)) {
_declarations.add(node);
}
}

bool _withEmptyDescription(ArgumentList args) =>
args.arguments.any((argument) =>
argument is NamedExpression &&
argument.name.label.name == 'desc' &&
argument.expression is SimpleStringLiteral &&
(argument.expression as SimpleStringLiteral).value.isEmpty) ||
args.arguments.every((argument) =>
argument is! NamedExpression || argument.name.label.name != 'desc');
}
@@ -0,0 +1,83 @@
class SomeClassI18n {
static final String message = Intl.message(
'message',
name: 'SomeClassI18n_message',
desc: 'Message description',
);

static String plural = Intl.plural(
1,
one: 'one',
other: 'other',
name: 'SomeClassI18n_plural',
desc: 'Plural description',
);

static String gender = Intl.gender(
'other',
female: 'female',
male: 'male',
other: 'other',
name: 'SomeClassI18n_gender',
desc: 'Gender description',
);

static String select = Intl.select(
true,
{true: 'true', false: 'false'},
name: 'SomeClassI18n_select',
desc: 'Select description',
);
}

class Intl {
static String message(String messageText,
{String? desc = '',
Map<String, Object>? examples,
String? locale,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';

static String plural(num howMany,
{String? zero,
String? one,
String? two,
String? few,
String? many,
required String other,
String? desc,
Map<String, Object>? examples,
String? locale,
int? precision,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';

static String gender(String targetGender,
{String? female,
String? male,
required String other,
String? desc,
Map<String, Object>? examples,
String? locale,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';

static String select(Object choice, Map<Object, String> cases,
{String? desc,
Map<String, Object>? examples,
String? locale,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';
}
@@ -0,0 +1,111 @@
class SomeClassI18n {
static final String message = Intl.message(
'message',
name: 'SomeClassI18n_message',
desc: '',
);

static final String message2 = Intl.message(
'message2',
name: 'SomeClassI18n_message2',
);

static String plural = Intl.plural(
1,
one: 'one',
other: 'other',
name: 'SomeClassI18n_plural',
desc: '',
);

static String plural2 = Intl.plural(
2,
one: 'one',
other: 'other',
name: 'SomeClassI18n_plural2',
);

static String gender = Intl.gender(
'other',
female: 'female',
male: 'male',
other: 'other',
name: 'SomeClassI18n_gender',
desc: '',
);

static String gender2 = Intl.gender(
'other',
female: 'female',
male: 'male',
other: 'other',
name: 'SomeClassI18n_gender2',
);

static String select = Intl.select(
true,
{true: 'true', false: 'false'},
name: 'SomeClassI18n_select',
desc: '',
);

static String select2 = Intl.select(
false,
{true: 'true', false: 'false'},
name: 'SomeClassI18n_select',
);
}

class Intl {
Intl();

static String message(String messageText,
{String? desc = '',
Map<String, Object>? examples,
String? locale,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';

static String plural(num howMany,
{String? zero,
String? one,
String? two,
String? few,
String? many,
required String other,
String? desc,
Map<String, Object>? examples,
String? locale,
int? precision,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';

static String gender(String targetGender,
{String? female,
String? male,
required String other,
String? desc,
Map<String, Object>? examples,
String? locale,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';

static String select(Object choice, Map<Object, String> cases,
{String? desc,
Map<String, Object>? examples,
String? locale,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
'';
}
@@ -0,0 +1,96 @@
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/prefer_provide_intl_description/prefer_provide_intl_description_rule.dart';
import 'package:test/test.dart';

import '../../../../../helpers/rule_test_helper.dart';

const _examplePath = 'prefer_provide_intl_description/examples/example.dart';
const _incorrectExamplePath =
'prefer_provide_intl_description/examples/incorrect_example.dart';

void main() {
group('$PreferProvideIntlDescriptionRule', () {
test('initialization', () async {
final unit = await RuleTestHelper.resolveFromFile(_incorrectExamplePath);
final issues = PreferProvideIntlDescriptionRule().check(unit);

RuleTestHelper.verifyInitialization(
issues: issues,
ruleId: 'prefer-provide-intl-description',
severity: Severity.warning,
);
});

test('reports no issues', () async {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = PreferProvideIntlDescriptionRule().check(unit);

RuleTestHelper.verifyNoIssues(issues);
});

test('reports about found issues for incorrect names', () async {
final unit = await RuleTestHelper.resolveFromFile(_incorrectExamplePath);
final issues = PreferProvideIntlDescriptionRule().check(unit);

RuleTestHelper.verifyIssues(
issues: issues,
startLines: [2, 8, 13, 21, 28, 37, 45, 52],
startColumns: [33, 34, 26, 27, 26, 27, 26, 27],
locationTexts: [
'Intl.message(\n'
" 'message',\n"
" name: 'SomeClassI18n_message',\n"
" desc: '',\n"
' )',
'Intl.message(\n'
" 'message2',\n"
" name: 'SomeClassI18n_message2',\n"
' )',
'Intl.plural(\n'
' 1,\n'
" one: 'one',\n"
" other: 'other',\n"
" name: 'SomeClassI18n_plural',\n"
" desc: '',\n"
' )',
'Intl.plural(\n'
' 2,\n'
" one: 'one',\n"
" other: 'other',\n"
" name: 'SomeClassI18n_plural2',\n"
' )',
'Intl.gender(\n'
" 'other',\n"
" female: 'female',\n"
" male: 'male',\n"
" other: 'other',\n"
" name: 'SomeClassI18n_gender',\n"
" desc: '',\n"
' )',
'Intl.gender(\n'
" 'other',\n"
" female: 'female',\n"
" male: 'male',\n"
" other: 'other',\n"
" name: 'SomeClassI18n_gender2',\n"
' )',
'Intl.select(\n'
' true,\n'
" {true: 'true', false: 'false'},\n"
" name: 'SomeClassI18n_select',\n"
" desc: '',\n"
' )',
'Intl.select(\n'
' false,\n'
" {true: 'true', false: 'false'},\n"
" name: 'SomeClassI18n_select',\n"
' )',
],
messages: List.filled(
issues.length,
'Provide description for translated message',
),
);
});
});
}

0 comments on commit 29e6181

Please sign in to comment.