diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b57a1ea76..5912bab947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* feat: add static code diagnostic [`prefer-correct-test-file-name`](https://dartcodemetrics.dev/docs/rules/common/prefer-correct-test-file-name). * feat: add static code diagnostic [`prefer-iterable-of`](https://dartcodemetrics.dev/docs/rules/common/prefer-iterable-of). ## 4.18.3 diff --git a/lib/src/analyzers/lint_analyzer/lint_utils.dart b/lib/src/analyzers/lint_analyzer/lint_utils.dart index 3453a99bae..15312ae5bc 100644 --- a/lib/src/analyzers/lint_analyzer/lint_utils.dart +++ b/lib/src/analyzers/lint_analyzer/lint_utils.dart @@ -13,5 +13,11 @@ Iterable readExcludes(Map config) { : const []; } +bool hasExcludes(Map config) { + final data = config['exclude']; + + return _isIterableOfStrings(data); +} + bool _isIterableOfStrings(Object? object) => object is Iterable && object.every((node) => node is String); diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart index d4e0840fb4..0b38a15dce 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart @@ -45,6 +45,7 @@ import 'rules_list/prefer_conditional_expressions/prefer_conditional_expressions import 'rules_list/prefer_const_border_radius/prefer_const_border_radius_rule.dart'; import 'rules_list/prefer_correct_edge_insets_constructor/prefer_correct_edge_insets_constructor_rule.dart'; import 'rules_list/prefer_correct_identifier_length/prefer_correct_identifier_length_rule.dart'; +import 'rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule.dart'; import 'rules_list/prefer_correct_type_name/prefer_correct_type_name_rule.dart'; import 'rules_list/prefer_enums_by_name/prefer_enums_by_name_rule.dart'; import 'rules_list/prefer_extracting_callbacks/prefer_extracting_callbacks_rule.dart'; @@ -116,6 +117,7 @@ final _implementedRules = )>{ PreferCorrectEdgeInsetsConstructorRule.new, PreferCorrectIdentifierLengthRule.ruleId: PreferCorrectIdentifierLengthRule.new, + PreferCorrectTestFileNameRule.ruleId: PreferCorrectTestFileNameRule.new, PreferCorrectTypeNameRule.ruleId: PreferCorrectTypeNameRule.new, PreferEnumsByNameRule.ruleId: PreferEnumsByNameRule.new, PreferExtractingCallbacksRule.ruleId: PreferExtractingCallbacksRule.new, diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_top_level_members_in_tests/avoid_top_level_members_in_tests_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_top_level_members_in_tests/avoid_top_level_members_in_tests_rule.dart index 7811a96fdb..d8e76f53b4 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_top_level_members_in_tests/avoid_top_level_members_in_tests_rule.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_top_level_members_in_tests/avoid_top_level_members_in_tests_rule.dart @@ -22,7 +22,8 @@ class AvoidTopLevelMembersInTestsRule extends CommonRule { : super( id: ruleId, severity: readSeverity(config, Severity.warning), - excludes: ['/**', '!test/**'], + excludes: + hasExcludes(config) ? readExcludes(config) : ['/**', '!test/**'], ); @override diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/config_parser.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/config_parser.dart new file mode 100644 index 0000000000..98aa59485f --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/config_parser.dart @@ -0,0 +1,11 @@ +part of 'prefer_correct_test_file_name_rule.dart'; + +class _ConfigParser { + static const _namePatternConfig = 'name-pattern'; + + static String parseNamePattern(Map config) { + final raw = config[_namePatternConfig]; + + return raw is String ? raw : '_test.dart'; + } +} diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule.dart new file mode 100644 index 0000000000..ca7b8abd9a --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule.dart @@ -0,0 +1,47 @@ +// 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/common_rule.dart'; +import '../../rule_utils.dart'; + +part 'config_parser.dart'; +part 'visitor.dart'; + +class PreferCorrectTestFileNameRule extends CommonRule { + static const String ruleId = 'prefer-correct-test-file-name'; + + static const _warningMessage = 'Test file name should end with '; + + final String _fileNamePattern; + + PreferCorrectTestFileNameRule([Map config = const {}]) + : _fileNamePattern = _ConfigParser.parseNamePattern(config), + super( + id: ruleId, + severity: readSeverity(config, Severity.warning), + excludes: + hasExcludes(config) ? readExcludes(config) : ['/**', '!test/**'], + ); + + @override + Iterable check(InternalResolvedUnitResult source) { + final visitor = _Visitor(source.path, _fileNamePattern); + + source.unit.visitChildren(visitor); + + return visitor.declaration + .map((declaration) => createIssue( + rule: this, + location: nodeLocation(node: declaration, source: source), + message: '$_warningMessage$_fileNamePattern', + )) + .toList(growable: false); + } +} diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/visitor.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/visitor.dart new file mode 100644 index 0000000000..53005544b8 --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/visitor.dart @@ -0,0 +1,24 @@ +part of 'prefer_correct_test_file_name_rule.dart'; + +class _Visitor extends GeneralizingAstVisitor { + final String path; + final String pattern; + + final _declarations = []; + + Iterable get declaration => _declarations; + + _Visitor(this.path, this.pattern); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // ignore: deprecated_member_use + if (node.name.name != 'main' || _matchesTestName(path)) { + return; + } + + _declarations.add(node); + } + + bool _matchesTestName(String path) => path.endsWith(pattern); +} diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_match_file_name/prefer_match_file_name_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_match_file_name/prefer_match_file_name_rule.dart index dc17fb0448..53f206c235 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_match_file_name/prefer_match_file_name_rule.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_match_file_name/prefer_match_file_name_rule.dart @@ -29,6 +29,7 @@ class PreferMatchFileNameRule extends CommonRule { @override Iterable check(InternalResolvedUnitResult source) { final visitor = _Visitor(); + source.unit.visitChildren(visitor); final issues = []; diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/examples/correct_example.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/examples/correct_example.dart new file mode 100644 index 0000000000..ab73b3a234 --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/examples/correct_example.dart @@ -0,0 +1 @@ +void main() {} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/examples/example.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/examples/example.dart new file mode 100644 index 0000000000..946585b85c --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/examples/example.dart @@ -0,0 +1,4 @@ +// LINT +void main() { + print('Hello'); +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule_test.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule_test.dart new file mode 100644 index 0000000000..d76d5f4924 --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule_test.dart @@ -0,0 +1,54 @@ +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_correct_test_file_name/prefer_correct_test_file_name_rule.dart'; +import 'package:test/test.dart'; + +import '../../../../../helpers/rule_test_helper.dart'; + +const _examplePath = 'prefer_correct_test_file_name/examples/example.dart'; +const _correctExamplePath = + 'prefer_correct_test_file_name/examples/correct_example.dart'; + +void main() { + group('PreferCorrectTestFileNameRule', () { + test('initialization', () async { + final unit = await RuleTestHelper.resolveFromFile(_examplePath); + final issues = PreferCorrectTestFileNameRule().check(unit); + + RuleTestHelper.verifyInitialization( + issues: issues, + ruleId: 'prefer-correct-test-file-name', + severity: Severity.warning, + ); + }); + + test('reports about found issues', () async { + final unit = await RuleTestHelper.resolveFromFile(_examplePath); + final config = {'name-pattern': 'correct_example.dart'}; + + final issues = PreferCorrectTestFileNameRule(config).check(unit); + + RuleTestHelper.verifyIssues( + issues: issues, + startLines: [2], + startColumns: [1], + locationTexts: [ + 'void main() {\n' + " print('Hello');\n" + '}', + ], + messages: [ + 'Test file name should end with correct_example.dart', + ], + ); + }); + + test('reports no found issues', () async { + final unit = await RuleTestHelper.resolveFromFile(_correctExamplePath); + final config = {'name-pattern': 'correct_example.dart'}; + + final issues = PreferCorrectTestFileNameRule(config).check(unit); + + RuleTestHelper.verifyNoIssues(issues); + }); + }); +} diff --git a/website/docs/rules/common/avoid-top-level-members-in-tests.mdx b/website/docs/rules/common/avoid-top-level-members-in-tests.mdx index c0b2252983..32f1a2d11b 100644 --- a/website/docs/rules/common/avoid-top-level-members-in-tests.mdx +++ b/website/docs/rules/common/avoid-top-level-members-in-tests.mdx @@ -6,6 +6,12 @@ Warns when a public top-level member (expect the entrypoint) is declared inside It helps reduce code bloat and find unused declarations in test files. +::: note + +If you want to set `exclude` config for this rule, the default `['/**', '!test/**']` will be overriden. + +::: + ### Example {#example} Bad: diff --git a/website/docs/rules/common/prefer-correct-test-file-name.mdx b/website/docs/rules/common/prefer-correct-test-file-name.mdx new file mode 100644 index 0000000000..e820873c0d --- /dev/null +++ b/website/docs/rules/common/prefer-correct-test-file-name.mdx @@ -0,0 +1,41 @@ +import RuleDetails from '@site/src/components/RuleDetails'; + + + +Warns if the file within `/test` contains a `main`, but the file name doesn't end with `_test.dart`. + +:::note + +If you want to set `exclude` config for this rule, the default `['/**', '!test/**']` will be overriden. + +::: + +### Example {#example} + +**❌ Bad:** + +File name: **some_file.dart** + +```dart +void main() { + ... +} +``` + +**✅ Good:** + +File name: **some_file_test.dart** + +```dart +void main() { + ... +} +``` + +File name: **some_other_file.dart** + +```dart +void helperFunction() { + ... +} +``` diff --git a/website/docs/rules/index.mdx b/website/docs/rules/index.mdx index 9bd6bd9fbf..b4bf6b41bc 100644 --- a/website/docs/rules/index.mdx +++ b/website/docs/rules/index.mdx @@ -355,6 +355,16 @@ Rules are grouped by category to help you understand their purpose. Each rule ha Warns when an identifier name length is very short or long. + + Warns if the file within /test contains a main, but + the file name doesn't end with _test.dart. + +