diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 3f3b5017..2a0615f7 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -52,6 +52,7 @@ dart_code_linter: - avoid-unnecessary-type-casts - avoid-unused-parameters - newline-before-return + - no-blank-line-before-single-return - no-boolean-literal-compare - no-empty-block - no-equal-then-else diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart index e0be3647..c8ac366b 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart @@ -44,6 +44,7 @@ import 'rules_list/list_all_equatable_fields/list_all_equatable_fields_rule.dart import 'rules_list/member_ordering/member_ordering_rule.dart'; import 'rules_list/missing_test_assertion/missing_test_assertion_rule.dart'; import 'rules_list/newline_before_return/newline_before_return_rule.dart'; +import 'rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule.dart'; import 'rules_list/no_boolean_literal_compare/no_boolean_literal_compare_rule.dart'; import 'rules_list/no_empty_block/no_empty_block_rule.dart'; import 'rules_list/no_equal_arguments/no_equal_arguments_rule.dart'; @@ -130,6 +131,7 @@ final _implementedRules = )>{ MemberOrderingRule.ruleId: MemberOrderingRule.new, MissingTestAssertionRule.ruleId: MissingTestAssertionRule.new, NewlineBeforeReturnRule.ruleId: NewlineBeforeReturnRule.new, + NoBlankLineBeforeSingleReturnRule.ruleId: NoBlankLineBeforeSingleReturnRule.new, NoBooleanLiteralCompareRule.ruleId: NoBooleanLiteralCompareRule.new, NoEmptyBlockRule.ruleId: NoEmptyBlockRule.new, NoEqualArgumentsRule.ruleId: NoEqualArgumentsRule.new, diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule.dart new file mode 100644 index 00000000..85b1a779 --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule.dart @@ -0,0 +1,99 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/source/line_info.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/dart_rule.dart'; +import '../../rule_utils.dart'; + +part 'visitor.dart'; + +class NoBlankLineBeforeSingleReturnRule extends DartRule { + static const String ruleId = 'no-blank-line-before-single-return'; + + static const warning = 'Remove blank line before single return statement in a block.'; + + NoBlankLineBeforeSingleReturnRule([Map config = const {}]) + : super( + id: ruleId, + severity: readSeverity(config, Severity.style), + excludes: readExcludes(config), + includes: readIncludes(config), + ); + + @override + Iterable check(InternalResolvedUnitResult source) { + final visitor = _Visitor(); + + source.unit.visitChildren(visitor); + + return visitor.statements + // Ensure the return statement is in a block + .where((statement) => statement.parent is Block) + // Ensure the return statement is the only statement in the block + .where((statement) { + final parentBlock = statement.parent as Block; + + return parentBlock.statements.length == 1; + }) + // Ensure there is no blank line before the return statement, ignoring comments + .where((statement) { + final lineInfo = source.lineInfo; + + // Get the last non-comment token before the return statement + final previousTokenLine = lineInfo + .getLocation(statement.returnKeyword.previous!.end) + .lineNumber; + + final tokenLine = lineInfo + .getLocation( + _optimalToken(statement.returnKeyword, lineInfo).offset, + ) + .lineNumber; + + return tokenLine != previousTokenLine + 1; + }) + .map((statement) => createIssue( + rule: this, + location: nodeLocation(node: statement, source: source), + message: warning, + )) + .toList(growable: false); + } +} + +Token _optimalToken(Token token, LineInfo lineInfo) { + var optimalToken = token; + var commentToken = _latestCommentToken(token); + + while (commentToken != null) { + final commentTokenLineNumber = lineInfo.getLocation(commentToken.end).lineNumber; + final optimalTokenLineNumber = lineInfo.getLocation(optimalToken.offset).lineNumber; + + final isDirectlyPrecedingComment = commentTokenLineNumber + 1 >= optimalTokenLineNumber; + + if (!isDirectlyPrecedingComment) { + break; + } + + optimalToken = commentToken; + commentToken = commentToken.previous; + } + + return optimalToken; +} + +Token? _latestCommentToken(Token token) { + Token? latestCommentToken = token.precedingComments; + + while (latestCommentToken?.next != null) { + latestCommentToken = latestCommentToken?.next; + } + + return latestCommentToken; +} diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/visitor.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/visitor.dart new file mode 100644 index 00000000..3b646125 --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/visitor.dart @@ -0,0 +1,13 @@ +part of 'no_blank_line_before_single_return_rule.dart'; + +class _Visitor extends RecursiveAstVisitor { + final _statements = []; + + Iterable get statements => _statements; + + @override + void visitReturnStatement(ReturnStatement node) { + super.visitReturnStatement(node); + _statements.add(node); + } +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/examples/example.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/examples/example.dart new file mode 100644 index 00000000..84c594cb --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/examples/example.dart @@ -0,0 +1,134 @@ +// ignore_for_file: always_put_control_body_on_new_line, newline-before-return +int simpleFunction() { + var a = 4; + + if (a > 70) { + /* multi line + comment */ + return a + 1; + } else if (a > 65) { + a++; + /* multi line + comment */ + return a + 1; + } else if (a > 60) { + a++; + + /* multi line + comment */ + return a + 2; + } else if (a > 55) { + a--; + /* multi line + comment */ + + return a + 3; + } + + if (a > 50) { + // simple comment + // simple comment second line + return a + 1; + } else if (a > 45) { + a++; + // simple comment + // simple comment second line + + return a + 2; + } else if (a > 40) { + a++; + // simple comment + + // simple comment second line + return a + 2; + } else if (a > 35) { + a--; + + // simple comment + // simple comment second line + return a + 3; + } + + if (a > 30) { + // simple comment + return a + 1; + } else if (a > 25) { + a++; + // simple comment + return a + 2; + } else if (a > 20) { + a--; + + // simple comment + return a + 3; + } + + if (a > 15) { + return a + 1; + } else if (a > 10) { + a++; + return a + 2; + } else if (a > 5) { + a--; + + return a + 3; + } + + if (a > 5) { + + return a - 1; + } + + if (a > 5) { + + // one line comment + return a - 1; + } + + if (a > 5) { + + // one line comment + // one line comment + return a - 1; + } + + if (a > 5) { + + /* + * Multi line comment + * */ + return a - 1; + } + + if (a > 5) { + // one line comment + + return a - 1; + } + + if (a > 5) { + + // one line comment + + return a - 1; + } + + if (a > 5) { + + /* + * Multi line comment + * */ + + return a - 1; + } + + if (a > 4) return a + 1; + + if (a > 3) return a + 2; + + if (a > 2) { + return a + 3; + } + + return a; +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule_test.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule_test.dart new file mode 100644 index 00000000..e2e2433a --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule_test.dart @@ -0,0 +1,37 @@ +import 'package:dart_code_linter/src/analyzers/lint_analyzer/models/severity.dart'; +import 'package:dart_code_linter/src/analyzers/lint_analyzer/rules/rules_list/no_blank_line_before_single_return/no_blank_line_before_single_return_rule.dart'; +import 'package:test/test.dart'; + +import '../../../../../helpers/rule_test_helper.dart'; + +const _examplePath = 'no_blank_line_before_single_return/examples/example.dart'; + +void main() { + group('NoBlankLineBeforeSingleReturnRule', () { + test('initialization', () async { + final unit = await RuleTestHelper.resolveFromFile(_examplePath); + final issues = NoBlankLineBeforeSingleReturnRule().check(unit); + + RuleTestHelper.verifyInitialization( + issues: issues, + ruleId: NoBlankLineBeforeSingleReturnRule.ruleId, + severity: Severity.style, + ); + }); + + test('reports about found issues', () async { + final unit = await RuleTestHelper.resolveFromFile(_examplePath); + final issues = NoBlankLineBeforeSingleReturnRule().check(unit); + + List startLines = [79, 85, 92, 100, 106, 113, 122]; + + RuleTestHelper.verifyIssues( + issues: issues, + startLines: startLines, + startColumns: List.generate(startLines.length, (index) => 5), + locationTexts: List.generate(startLines.length, (index) => 'return a - 1;'), + messages: List.generate(startLines.length, (index) => NoBlankLineBeforeSingleReturnRule.warning), + ); + }); + }); +}