Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a rule prefer_guard_clause for reversing nested if statements #154

Merged
merged 12 commits into from
Apr 22, 2024
1 change: 1 addition & 0 deletions lib/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ custom_lint:
- newline_before_return
- no_empty_block
- no_equal_then_else
- prefer_guard_clause

- no_magic_number:
allowed_in_widget_params: true
Expand Down
2 changes: 2 additions & 0 deletions lib/solid_lints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart';
import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart';
import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart';
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';
import 'package:solid_lints/src/lints/prefer_guard_clause/prefer_guard_clause_rule.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';

/// Creates a plugin for our custom linter
Expand Down Expand Up @@ -61,6 +62,7 @@ class _SolidLints extends PluginBase {
PreferMatchFileNameRule.createRule(configs),
ProperSuperCallsRule.createRule(configs),
AvoidDebugPrint.createRule(configs),
PreferGuardClauseRule.createRule(configs),
];

// Return only enabled rules
Expand Down
72 changes: 72 additions & 0 deletions lib/src/lints/prefer_guard_clause/prefer_guard_clause_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/src/lints/prefer_guard_clause/visitors/prefer_guard_clause_visitor.dart';
import 'package:solid_lints/src/models/rule_config.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';

/// A `prefer_guard_clause` rule which promotes using guard clauses
/// over nesting `if` statements
///
/// ### Example
///
/// #### BAD:
///
/// ```dart
/// void func() {
/// if(a){ //LINT
/// if(b){ //LINT
/// c;
/// }
/// }
/// }
/// ```
///
/// #### GOOD:
///
/// ```dart
/// void func() {
/// if(!a) return;
/// if(!b) return;
/// c;
/// }
/// ```
class PreferGuardClauseRule extends SolidLintRule {
/// The [LintCode] of this lint rule that represents the error if
/// 'if' statements should be reversed
static const String lintName = 'prefer_guard_clause';

PreferGuardClauseRule._(super.config);

/// Creates a new instance of [PreferGuardClauseRule]
/// based on the lint configuration.
factory PreferGuardClauseRule.createRule(CustomLintConfigs configs) {
final rule = RuleConfig(
configs: configs,
name: lintName,
problemMessage: (_) =>
"Prefer using guard clause over nesting `if` statements",
);

return PreferGuardClauseRule._(rule);
}

@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addDeclaration((node) {
if (node is! FunctionDeclaration && node is! MethodDeclaration) {
velvitoff marked this conversation as resolved.
Show resolved Hide resolved
return;
}
final visitor = PreferGuardClauseVisitor();
node.accept(visitor);

for (final element in visitor.nodes) {
reporter.reportErrorForNode(code, element);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:solid_lints/src/lints/prefer_guard_clause/visitors/return_statement_visitor.dart';

/// The AST visitor that will collect all unnecessary if statements and
/// conditional expressions.
class PreferGuardClauseVisitor extends RecursiveAstVisitor<void> {
final _nodes = <AstNode>[];

/// All unnecessary if statements and conditional expressions.
Iterable<AstNode> get nodes => _nodes;

@override
void visitFunctionDeclaration(FunctionDeclaration node) {
super.visitFunctionDeclaration(node);
_handleFunctionBody(node.functionExpression.body);
}

@override
void visitMethodDeclaration(MethodDeclaration node) {
super.visitMethodDeclaration(node);
_handleFunctionBody(node.body);
}

void _handleFunctionBody(FunctionBody body) {
if (body is! BlockFunctionBody) return;
if (body.block.statements.isEmpty) return;

final first = body.block.statements.first;
var last = body.block.statements.last;

switch (body.block.statements.length) {
case 1:
if (first is IfStatement) _handleIfStatement(first);
return;
case 2:
break;
case _:
if (last is ReturnStatement) {
last = body.block.statements[body.block.statements.length - 2];
velvitoff marked this conversation as resolved.
Show resolved Hide resolved
}
}

if (first is IfStatement) _handleIfStatement(first);
if (last is IfStatement) _handleIfStatement(last);
}

void _handleIfStatement(IfStatement node) {
if (_isElseIfStatement(node)) return;

if (_hasElseStatement(node)) {
//if both statements don't contain return statements -> ignore
//if at least one statement contains return statemnt -> highlight
if (!_containsReturnStatement(node)) return;
} else {
return;
}

_nodes.add(node);
}
}

bool _hasElseStatement(IfStatement node) {
return node.elseStatement != null;
}

bool _isElseIfStatement(IfStatement node) {
return node.elseStatement != null && node.elseStatement is IfStatement;
}

bool _containsReturnStatement(Statement node) {
final visitor = ReturnStatementVisitor();
node.accept(visitor);
return visitor.nodes.isNotEmpty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';

/// The AST visitor that will collect every return statement
class ReturnStatementVisitor extends RecursiveAstVisitor<void> {
final _nodes = <ReturnStatement>[];

/// All unnecessary if statements and conditional expressions.
Iterable<ReturnStatement> get nodes => _nodes;

@override
void visitReturnStatement(ReturnStatement node) {
super.visitReturnStatement(node);
_nodes.add(node);
}
}
1 change: 1 addition & 0 deletions lint_test/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ custom_lint:
- no_empty_block
- no_equal_then_else
- avoid_debug_print
- prefer_guard_clause
- member_ordering:
alphabetize: true
order:
Expand Down
2 changes: 1 addition & 1 deletion lint_test/cyclomatic_complexity_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// ignore_for_file: literal_only_boolean_expressions
// ignore_for_file: literal_only_boolean_expressions, prefer_guard_clause
// ignore_for_file: no_empty_block, prefer_match_file_name

/// Check complexity fail
Expand Down
Loading