Skip to content

Commit 69ea981

Browse files
authored
Add new rule: dont_create_a_return_var (#220)
* Issue #92, new rule `dont_create_a_return_var`: - added rule class (no rule implementation) - added test cases * Issue #92: added rule in analysis_options.yaml * Issue #92: added new test case * Issue #92: added naive implementation for dont_create_a_return_var_rule * Issue #92: fixed conflicting lints in test * Issue #92: added test case where return var defined from mutable, but return goes just after (bad practice, not valid caching case) * Issue #92: implemented advanced rule with visitor, checking where variable is defined as well as how often is used * Issue #92: fixed formatting with 'dart format' * Issue #92: changed if chain to switch statement (more readable) * Issue #92: renaming rule to avoid_unnecessary_return_variable * Issue #92: small readability improvements * #Issue 92: renamed test file accordingly
1 parent ee3e27d commit 69ea981

File tree

6 files changed

+363
-0
lines changed

6 files changed

+363
-0
lines changed

lib/analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ custom_lint:
4545

4646
- avoid_non_null_assertion
4747
- avoid_returning_widgets
48+
- avoid_unnecessary_return_variable
4849
- avoid_unnecessary_setstate
4950
- avoid_unnecessary_type_assertions
5051
- avoid_unnecessary_type_casts

lib/solid_lints.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule
77
import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart';
88
import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart';
99
import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart';
10+
import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart';
1011
import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart';
1112
import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart';
1213
import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart';
@@ -67,6 +68,7 @@ class _SolidLints extends PluginBase {
6768
PreferEarlyReturnRule.createRule(configs),
6869
AvoidFinalWithGetterRule.createRule(configs),
6970
NamedParametersOrderingRule.createRule(configs),
71+
AvoidUnnecessaryReturnVariableRule.createRule(configs),
7072
];
7173

7274
// Return only enabled rules
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/element/element2.dart';
3+
import 'package:analyzer/error/listener.dart';
4+
import 'package:custom_lint_builder/custom_lint_builder.dart';
5+
import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart';
6+
import 'package:solid_lints/src/models/rule_config.dart';
7+
import 'package:solid_lints/src/models/solid_lint_rule.dart';
8+
9+
/// An `avoid_unnecessary_return_variable` rule which forbids returning
10+
/// an immutable variable if it can be rewritten in return statement itself.
11+
///
12+
/// See more here: https://github.com/solid-software/solid_lints/issues/92
13+
///
14+
/// ### Example
15+
///
16+
/// #### BAD:
17+
///
18+
/// ```dart
19+
/// void x() {
20+
/// final y = 1;
21+
///
22+
/// return y;
23+
/// }
24+
/// ```
25+
///
26+
/// #### GOOD:
27+
///
28+
/// ```dart
29+
/// void x() {
30+
/// return 1;
31+
/// }
32+
/// ```
33+
///
34+
class AvoidUnnecessaryReturnVariableRule extends SolidLintRule {
35+
/// This lint rule represents the error
36+
/// when unnecessary return variable statement is found
37+
static const lintName = 'avoid_unnecessary_return_variable';
38+
39+
AvoidUnnecessaryReturnVariableRule._(super.config);
40+
41+
/// Creates a new instance of [AvoidUnnecessaryReturnVariableRule]
42+
/// based on the lint configuration.
43+
factory AvoidUnnecessaryReturnVariableRule.createRule(
44+
CustomLintConfigs configs,
45+
) {
46+
final rule = RuleConfig(
47+
configs: configs,
48+
name: lintName,
49+
problemMessage: (_) => """
50+
Avoid creating unnecessary variable only for return.
51+
Rewrite the variable evaluation into return statement instead.""",
52+
);
53+
54+
return AvoidUnnecessaryReturnVariableRule._(rule);
55+
}
56+
57+
@override
58+
void run(
59+
CustomLintResolver resolver,
60+
ErrorReporter reporter,
61+
CustomLintContext context,
62+
) {
63+
context.registry.addReturnStatement(
64+
(statement) {
65+
_checkReturnStatement(
66+
statement: statement,
67+
reporter: reporter,
68+
context: context,
69+
);
70+
},
71+
);
72+
}
73+
74+
void _checkReturnStatement({
75+
required ReturnStatement statement,
76+
required ErrorReporter reporter,
77+
required CustomLintContext context,
78+
}) {
79+
final expr = statement.expression;
80+
if (expr is! SimpleIdentifier) return;
81+
82+
final element = expr.element;
83+
if (element is! LocalVariableElement2) return;
84+
final returnVariableElement = element;
85+
86+
if (!returnVariableElement.isFinal && !returnVariableElement.isConst) {
87+
return;
88+
}
89+
90+
final blockBody = statement.parent;
91+
if (blockBody == null) return;
92+
93+
final visitor = AvoidUnnecessaryReturnVariableVisitor(
94+
returnVariableElement,
95+
statement,
96+
);
97+
blockBody.visitChildren(visitor);
98+
99+
if (!visitor.hasBadStatementCount()) return;
100+
101+
//it is 100% bad if return statement follows declaration
102+
if (!visitor.foundTokensBetweenDeclarationAndReturn) {
103+
reporter.atNode(statement, code);
104+
return;
105+
}
106+
107+
final declaration = visitor.variableDeclaration;
108+
109+
//check if immutable
110+
final initializer = declaration?.initializer;
111+
if (initializer == null) return;
112+
113+
if (!_isExpressionImmutable(initializer)) return;
114+
115+
reporter.atNode(statement, code);
116+
}
117+
118+
bool _isExpressionImmutable(Expression expr) {
119+
switch (expr) {
120+
case Literal _:
121+
return true;
122+
123+
case final PrefixedIdentifier prefixed:
124+
return _isExpressionImmutable(prefixed.prefix) &&
125+
_isExpressionImmutable(prefixed.identifier);
126+
127+
case final BinaryExpression binExpr:
128+
return _isExpressionImmutable(binExpr.leftOperand) &&
129+
_isExpressionImmutable(binExpr.rightOperand);
130+
131+
case final SimpleIdentifier identifier:
132+
return _isSimpleIdentifierImmutable(identifier);
133+
}
134+
135+
return false;
136+
}
137+
138+
bool _isSimpleIdentifierImmutable(SimpleIdentifier identifier) {
139+
switch (identifier.element) {
140+
case final VariableElement2 variable:
141+
return variable.isFinal || variable.isConst;
142+
143+
case ClassElement2 _:
144+
return true;
145+
146+
case final GetterElement getter:
147+
if (getter.variable3 case final PropertyInducingElement2 property
148+
when property.isFinal || property.isConst) {
149+
return true;
150+
}
151+
}
152+
153+
return false;
154+
}
155+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/visitor.dart';
3+
import 'package:analyzer/dart/element/element2.dart';
4+
import 'package:collection/collection.dart';
5+
6+
/// This visitor is searches all uses of a single local variable,
7+
/// including variable declaration.
8+
class AvoidUnnecessaryReturnVariableVisitor extends RecursiveAstVisitor<void> {
9+
/// The problem expects that exactly 1 mention of return variable.
10+
/// VariableDeclarationStatement doesn't count when visiting SimpleIdentifier.
11+
/// Any other amount of variable mentions implies that it is used somewhere
12+
/// except return, so its existance is justified.
13+
static const _badStatementCount = 1;
14+
15+
final LocalVariableElement2 _returnVariableElement;
16+
17+
bool _foundTokensBetweenDeclarationAndReturn = false;
18+
VariableDeclaration? _variableDeclaration;
19+
int _variableStatementCounter = 0;
20+
21+
final ReturnStatement _returnStatement;
22+
23+
/// After visiting holds info about whether there are any tokens
24+
/// between variable declaration and return statement
25+
bool get foundTokensBetweenDeclarationAndReturn =>
26+
_foundTokensBetweenDeclarationAndReturn;
27+
28+
/// Returns statement of local variable declaration
29+
VariableDeclaration? get variableDeclaration => _variableDeclaration;
30+
31+
/// Creates a new instance of [AvoidUnnecessaryReturnVariableVisitor].
32+
AvoidUnnecessaryReturnVariableVisitor(
33+
this._returnVariableElement,
34+
this._returnStatement,
35+
);
36+
37+
/// Defines whether the variables is used in return statement only.
38+
bool hasBadStatementCount() =>
39+
_variableStatementCounter == _badStatementCount;
40+
41+
@override
42+
void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
43+
if (_collectVariableDeclaration(node)) {
44+
_checkTokensInBetween(node, _returnStatement);
45+
}
46+
super.visitVariableDeclarationStatement(node);
47+
}
48+
49+
@override
50+
void visitSimpleIdentifier(SimpleIdentifier node) {
51+
if (node.element?.id == _returnVariableElement.id) {
52+
_variableStatementCounter++;
53+
}
54+
55+
super.visitSimpleIdentifier(node);
56+
}
57+
58+
bool _collectVariableDeclaration(VariableDeclarationStatement node) {
59+
final targetVariable = node.variables.variables.firstWhereOrNull(
60+
(v) => v.declaredElement2?.id == _returnVariableElement.id,
61+
);
62+
if (targetVariable == null) return false;
63+
64+
_variableDeclaration = targetVariable;
65+
return true;
66+
}
67+
68+
void _checkTokensInBetween(
69+
VariableDeclarationStatement variableDeclaration,
70+
ReturnStatement returnStatement,
71+
) {
72+
final tokenBeforeReturn =
73+
_returnStatement.findPrevious(_returnStatement.beginToken);
74+
75+
if (tokenBeforeReturn != variableDeclaration.endToken) {
76+
_foundTokensBetweenDeclarationAndReturn = true;
77+
}
78+
}
79+
}

lint_test/analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,4 @@ custom_lint:
8989
- required
9090
- nullable
9191
- default
92+
- avoid_unnecessary_return_variable
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// ignore_for_file: unused_local_variable, newline_before_return, no_empty_block
2+
3+
/// Test the avoid_unnecessary_return_variable.
4+
/// Good code, trivial case.
5+
int returnVarTestGoodTrivial() {
6+
return 1;
7+
}
8+
9+
/// Test the avoid_unnecessary_return_variable.
10+
/// Returning mutable variable should not trigger the lint.
11+
int returnVarTestReturnMutable() {
12+
var a = 1;
13+
a++;
14+
15+
return a;
16+
}
17+
18+
int returnVarTestReturnParameter(int param) {
19+
return param;
20+
}
21+
22+
/// Test the avoid_unnecessary_return_variable.
23+
/// Caching mutable variable value.
24+
/// Unpredictable: may be useful to cache value
25+
/// before operation can change it.
26+
int returnVarTestCachedMutable() {
27+
var a = 1;
28+
final result = a;
29+
_doNothing();
30+
31+
return result;
32+
}
33+
34+
/// Test the avoid_unnecessary_return_variable.
35+
/// Caching mutable variable value, but return goes
36+
/// right after declaration, which makes it bad.
37+
int returnVarTestReturnFollowsDeclaration() {
38+
var a = 1;
39+
final result = a;
40+
41+
//Some comment here
42+
43+
//expect_lint: avoid_unnecessary_return_variable
44+
return result;
45+
}
46+
47+
/// Test the avoid_unnecessary_return_variable.
48+
/// Caching another method result.
49+
/// Unpredictable: may be useful to cache value
50+
/// before operation can change it.
51+
int returnVarTestCachedAnotherMethodResult() {
52+
var a = 1;
53+
final result = _testValueEval();
54+
_doNothing();
55+
56+
return result;
57+
}
58+
59+
/// Test the avoid_unnecessary_return_variable.
60+
/// Caching value of object's field.
61+
/// Unpredictable: may be useful to cache value
62+
/// before operation can change it.
63+
int returnVarTestCachedObjectField() {
64+
final obj = _TestClass();
65+
final result = obj.varField;
66+
_doNothing();
67+
68+
return result;
69+
}
70+
71+
/// Test the avoid_unnecessary_return_variable.
72+
/// Good: variable is created not only for return
73+
/// but is used in following expressions as well.
74+
int returnVarTestUsedVariable() {
75+
var a = 1;
76+
final result = 2;
77+
a += result;
78+
79+
return result;
80+
}
81+
82+
/// Test the avoid_unnecessary_return_variable.
83+
/// Bad code, trivial example.
84+
int returnVarTestBadTrivial() {
85+
final result = 1;
86+
87+
//expect_lint: avoid_unnecessary_return_variable
88+
return result;
89+
}
90+
91+
/// Test the avoid_unnecessary_return_variable.
92+
/// Bad code: result expression is immutable,
93+
/// so can be written in return statement directly.
94+
int returnVarTestBadImmutableExpression() {
95+
const constLocal = 1;
96+
final finalLocal = 1;
97+
final testObj = _TestClass();
98+
final result = constLocal +
99+
finalLocal +
100+
1 + //const literal
101+
_TestClass.constValue +
102+
_TestClass.finalValue +
103+
testObj.finalField;
104+
_doNothing();
105+
106+
//expect_lint: avoid_unnecessary_return_variable
107+
return result;
108+
}
109+
110+
int _testValueEval() {
111+
return 1;
112+
}
113+
114+
/// This method is a placeholder for unpredictable behaviour
115+
/// which can potentially change any mutable variables
116+
void _doNothing() {}
117+
118+
//ignore: prefer_match_file_name
119+
class _TestClass {
120+
static const constValue = 1;
121+
static final finalValue = 1;
122+
//ignore: member_ordering
123+
final finalField = 1;
124+
var varField = 1;
125+
}

0 commit comments

Comments
 (0)