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

feat: introduce prefer-provide-intl-description rule #1137

Merged
merged 3 commits into from
Jan 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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');
}
Original file line number Diff line number Diff line change
@@ -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}) =>
'';
}
Original file line number Diff line number Diff line change
@@ -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}) =>
'';
}
Original file line number Diff line number Diff line change
@@ -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',
),
);
});
});
}
Loading