Skip to content
This repository was archived by the owner on Nov 20, 2024. It is now read-only.

Commit c108d70

Browse files
authored
Merge pull request #229 from alexeieleusis/invariant_boolean
Conditions should not unconditionally evaluate to "TRUE" or to "FALSE"
2 parents 183a56a + a9c1a88 commit c108d70

9 files changed

+817
-4
lines changed

lib/src/rules.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ import 'package:linter/src/rules/empty_constructor_bodies.dart';
2727
import 'package:linter/src/rules/empty_statements.dart';
2828
import 'package:linter/src/rules/hash_and_equals.dart';
2929
import 'package:linter/src/rules/implementation_imports.dart';
30+
import 'package:linter/src/rules/invariant_booleans.dart';
3031
import 'package:linter/src/rules/iterable_contains_unrelated_type.dart';
3132
import 'package:linter/src/rules/library_names.dart';
3233
import 'package:linter/src/rules/library_prefixes.dart';
3334
import 'package:linter/src/rules/list_remove_unrelated_type.dart';
35+
import 'package:linter/src/rules/literal_only_boolean_expressions.dart';
3436
import 'package:linter/src/rules/non_constant_identifier_names.dart';
3537
import 'package:linter/src/rules/one_member_abstracts.dart';
3638
import 'package:linter/src/rules/only_throw_errors.dart';
@@ -74,10 +76,12 @@ final Registry ruleRegistry = new Registry()
7476
..register(new EmptyStatements())
7577
..register(new HashAndEquals())
7678
..register(new ImplementationImports())
79+
..register(new InvariantBooleans())
7780
..register(new IterableContainsUnrelatedType())
7881
..register(new LibraryNames())
7982
..register(new LibraryPrefixes())
8083
..register(new ListRemoveUnrelatedType())
84+
..register(new LiteralOnlyBooleanExpressions())
8185
..register(new NonConstantIdentifierNames())
8286
..register(new OneMemberAbstracts())
8387
..register(new OnlyThrowErrors())

lib/src/rules/invariant_booleans.dart

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library linter.src.rules.invariant_booleans;
6+
7+
import 'package:analyzer/dart/ast/ast.dart';
8+
import 'package:analyzer/dart/ast/visitor.dart';
9+
import 'package:linter/src/linter.dart';
10+
import 'package:linter/src/util/boolean_expression_utilities.dart';
11+
import 'package:linter/src/util/dart_type_utilities.dart';
12+
import 'package:linter/src/util/tested_expressions.dart';
13+
14+
const _desc = r'Conditions should not unconditionally evaluate to "TRUE" or to "FALSE"';
15+
16+
const _details = r'''
17+
18+
**DON'T** test for conditions that can be inferred at compile time or test the
19+
same condition twice.
20+
Conditional statements using a condition which cannot be anything but FALSE have
21+
the effect of making blocks of code non-functional. If the condition cannot
22+
evaluate to anything but TRUE, the conditional statement is completely
23+
redundant, and makes the code less readable.
24+
It is quite likely that the code does not match the programmer's intent.
25+
Either the condition should be removed or it should be updated so that it does
26+
not always evaluate to TRUE or FALSE and does not perform redundant tests.
27+
This rule will hint to the test conflicting with the linted one.
28+
29+
**BAD:**
30+
```
31+
//foo can't be both equal and not equal to bar in the same expression
32+
if(foo == bar && something && foo != bar) {...}
33+
```
34+
35+
**BAD:**
36+
```
37+
void compute(int foo) {
38+
if (foo == 4) {
39+
doSomething();
40+
// We know foo is equal to 4 at this point, so the next condition is always false
41+
if (foo > 4) {...}
42+
...
43+
}
44+
...
45+
}
46+
```
47+
48+
**BAD:**
49+
```
50+
void compute(boolean foo) {
51+
if (foo) {
52+
return;
53+
}
54+
doSomething();
55+
// foo is always false here
56+
if (foo){...}
57+
...
58+
}
59+
```
60+
61+
**GOOD:**
62+
```
63+
void nestedOK() {
64+
if (foo == bar) {
65+
foo = baz;
66+
if (foo != bar) {...}
67+
}
68+
}
69+
```
70+
71+
**GOOD:**
72+
```
73+
void nestedOk2() {
74+
if (foo == bar) {
75+
return;
76+
}
77+
78+
foo = baz;
79+
if (foo == bar) {...} // OK
80+
}
81+
```
82+
83+
**GOOD:**
84+
```
85+
void nestedOk5() {
86+
if (foo != null) {
87+
if (bar != null) {
88+
return;
89+
}
90+
}
91+
92+
if (bar != null) {...} // OK
93+
}
94+
```
95+
''';
96+
97+
98+
class InvariantBooleans extends LintRule {
99+
_Visitor _visitor;
100+
101+
InvariantBooleans() : super(
102+
name: 'invariant_booleans',
103+
description: _desc,
104+
details: _details,
105+
group: Group.errors,
106+
maturity: Maturity.experimental) {
107+
_visitor = new _Visitor(this);
108+
}
109+
110+
@override
111+
AstVisitor getVisitor() => _visitor;
112+
}
113+
114+
class _Visitor extends SimpleAstVisitor {
115+
final LintRule rule;
116+
117+
_Visitor(this.rule);
118+
119+
_reportBinaryExpressionIfConstantValue(BinaryExpression node) {
120+
// Right part discards reporting a subexpression already reported.
121+
if (node.bestType.name != 'bool' || node.parent is! IfStatement) {
122+
return;
123+
}
124+
125+
TestedExpressions testedNodes = _findPreviousTestedExpressions(node);
126+
testedNodes.evaluateInvariant().forEach((ContradictoryComparisons e) {
127+
_ContradictionReportRule reportRule = new _ContradictionReportRule(e);
128+
reportRule.reporter = rule.reporter;
129+
reportRule.reportLint(e.second);
130+
});
131+
132+
// In dart booleanVariable == true is a valid comparison since the variable
133+
// can be null.
134+
if (!BooleanExpressionUtilities.EQUALITY_OPERATIONS
135+
.contains(node.operator.type) &&
136+
(node.leftOperand is BooleanLiteral ||
137+
node.rightOperand is BooleanLiteral)) {
138+
rule.reportLint(node);
139+
}
140+
}
141+
142+
@override
143+
visitForStatement(ForStatement node) {
144+
if (node.condition is BinaryExpression) {
145+
_reportBinaryExpressionIfConstantValue(node.condition);
146+
}
147+
}
148+
149+
@override
150+
visitDoStatement(DoStatement node) {
151+
if (node.condition is BinaryExpression) {
152+
_reportBinaryExpressionIfConstantValue(node.condition);
153+
}
154+
}
155+
156+
@override
157+
visitIfStatement(IfStatement node) {
158+
if (node.condition is BinaryExpression) {
159+
_reportBinaryExpressionIfConstantValue(node.condition);
160+
}
161+
}
162+
163+
@override
164+
visitWhileStatement(WhileStatement node) {
165+
if (node.condition is BinaryExpression) {
166+
_reportBinaryExpressionIfConstantValue(node.condition);
167+
}
168+
}
169+
170+
171+
}
172+
173+
/// The only purpose of this rule is to report the second node on a cotradictory
174+
/// comparison indicating the first node as the cause of the inconsistency.
175+
class _ContradictionReportRule extends LintRule {
176+
_ContradictionReportRule(ContradictoryComparisons comparisons) : super(
177+
name: 'invariant_boolean',
178+
description: _desc + ' verify: ${comparisons.first}.',
179+
details: _details,
180+
group: Group.errors,
181+
maturity: Maturity.experimental);
182+
}
183+
184+
TestedExpressions _findPreviousTestedExpressions(BinaryExpression node) {
185+
Block block = node
186+
.getAncestor((a) => a is Block && a.parent is BlockFunctionBody);
187+
Iterable<AstNode> nodesInDFS = DartTypeUtilities.traverseNodesInDFS(block);
188+
Iterable<Expression> conjunctions =
189+
_findConditionsOfIfStatementAncestor(node.parent, nodesInDFS).toSet();
190+
Iterable<Expression> negations =
191+
(_findConditionsCausingReturns(node, nodesInDFS)
192+
..addAll(_findConditionsOfElseStatementAncestor(node.parent, nodesInDFS)))
193+
.toSet();
194+
return new TestedExpressions(node, conjunctions, negations);
195+
}
196+
197+
Set<Expression> _findConditionsOfIfStatementAncestor(IfStatement statement,
198+
Iterable<AstNode> nodesInDFS) {
199+
return _findConditionsUnderIfStatementBranch(statement,
200+
(n) =>
201+
n is IfStatement &&
202+
statement.getAncestor((a) => a == n.thenStatement) != null, nodesInDFS);
203+
}
204+
205+
Set<Expression> _findConditionsOfElseStatementAncestor(IfStatement statement,
206+
Iterable<AstNode> nodesInDFS) {
207+
return _findConditionsUnderIfStatementBranch(statement,
208+
(n) =>
209+
n is IfStatement &&
210+
statement.getAncestor((a) => a == n.elseStatement) != null, nodesInDFS);
211+
}
212+
213+
Set<Expression> _findConditionsUnderIfStatementBranch(IfStatement statement,
214+
AstNodePredicate predicate, Iterable<AstNode> nodesInDFS) {
215+
AstNodePredicate noFurtherAssignmentInvalidatingCondition =
216+
_noFurtherAssignmentInvalidatingCondition(statement.condition, nodesInDFS);
217+
return nodesInDFS
218+
.where((n) => n == statement.getAncestor((a) => a == n && a != statement))
219+
.where((n) => n is IfStatement)
220+
.where(predicate)
221+
.where(noFurtherAssignmentInvalidatingCondition)
222+
.fold(<Expression>[],
223+
(List<Expression> previous, AstNode statement) {
224+
if (statement is IfStatement) {
225+
previous.add(statement.condition);
226+
}
227+
return previous;
228+
}).toSet();
229+
}
230+
231+
Set<Identifier> _findStatementIdentifiers(IfStatement statement) =>
232+
DartTypeUtilities.traverseNodesInDFS(statement)
233+
.where((n) => n is Identifier).toSet();
234+
235+
Set<Expression> _findConditionsCausingReturns(BinaryExpression node,
236+
Iterable<AstNode> nodesInDFS) =>
237+
nodesInDFS
238+
.takeWhile((n) => n != node.parent)
239+
.where(_isIfStatementWithReturn(nodesInDFS))
240+
.where((_noFurtherAssignmentInvalidatingCondition(node, nodesInDFS)))
241+
.fold(<Expression>[],
242+
(List<Expression> previous, AstNode statement) {
243+
if (statement is IfStatement) {
244+
previous.add(statement.condition);
245+
}
246+
return previous;
247+
}).toSet();
248+
249+
AstNodePredicate _isIfStatementWithReturn(Iterable<AstNode> blockNodes) =>
250+
(AstNode node) {
251+
Block block = node
252+
.getAncestor((a) => a is Block && a.parent is BlockFunctionBody);
253+
Iterable<AstNode> nodesInDFS = DartTypeUtilities.traverseNodesInDFS(node);
254+
return node is IfStatement
255+
&& nodesInDFS.any((n) => n is ReturnStatement)
256+
// Ignore nested if statements.
257+
&& !nodesInDFS.any((n) => n is IfStatement)
258+
&& node.getAncestor((n) =>
259+
n != node && n is IfStatement &&
260+
n.getAncestor((a) => a == block) == block) == null;
261+
};
262+
263+
AstNodePredicate _noFurtherAssignmentInvalidatingCondition(
264+
BinaryExpression node,
265+
Iterable<AstNode> nodesInDFS) {
266+
Set<Identifier> identifiers = _findStatementIdentifiers(node.parent);
267+
return (AstNode statement) =>
268+
nodesInDFS
269+
.skipWhile((n) => n != statement)
270+
.takeWhile((n) => n != node)
271+
.where((n) =>
272+
n is AssignmentExpression &&
273+
!identifiers.contains(n.leftHandSide))
274+
.length == 0;
275+
}

0 commit comments

Comments
 (0)