Skip to content

Commit 154dbfe

Browse files
committed
Lint to prefer x.isNotEmpty vs. !x.isEmpty (#143).
[email protected] Review URL: https://codereview.chromium.org//1419373009 .
1 parent 8e17ff7 commit 154dbfe

File tree

6 files changed

+165
-7
lines changed

6 files changed

+165
-7
lines changed

lib/src/ast.dart

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,30 @@ import 'package:analyzer/src/generated/ast.dart'
3131
TypeParameter,
3232
VariableDeclaration;
3333
import 'package:analyzer/src/generated/element.dart'
34-
show Element, ParameterElement, PropertyAccessorElement;
34+
show
35+
Element,
36+
GeneralizingElementVisitor,
37+
ParameterElement,
38+
PropertyAccessorElement;
3539
import 'package:analyzer/src/generated/scanner.dart'
3640
show Keyword, KeywordToken, KeywordTokenWithComment, Token;
3741
import 'package:linter/src/util.dart';
3842

43+
/// Returns direct children of [parent].
44+
List<Element> getChildren(Element parent, [String name]) {
45+
List<Element> children = <Element>[];
46+
visitChildren(parent, (Element element) {
47+
if (name == null || element.displayName == name) {
48+
children.add(element);
49+
}
50+
});
51+
return children;
52+
}
53+
3954
/// Returns the most specific AST node appropriate for associating errors.
4055
AstNode getNodeToAnnotate(Declaration node) {
4156
AstNode mostSpecific = _getNodeToAnnotate(node);
42-
if (mostSpecific != null) {
43-
return mostSpecific;
44-
}
45-
return node;
57+
return mostSpecific ?? node;
4658
}
4759

4860
/// Returns `true` if the keyword associated with this token is `final` or
@@ -131,6 +143,12 @@ bool isValidDartIdentifier(String id) => !isKeyWord(id) && isIdentifier(id);
131143
/// Returns `true` if the keyword associated with this token is `var`.
132144
bool isVar(Token token) => isKeyword(token, Keyword.VAR);
133145

146+
/// Uses [processor] to visit all of the children of [element].
147+
/// If [processor] returns `true`, then children of a child are visited too.
148+
void visitChildren(Element element, ElementProcessor processor) {
149+
element.visitChildren(new _ElementVisitorAdapter(processor));
150+
}
151+
134152
bool _checkForSimpleGetter(MethodDeclaration getter, Expression expression) {
135153
if (expression is SimpleIdentifier) {
136154
var staticElement = expression.staticElement;
@@ -224,3 +242,21 @@ AstNode _getNodeToAnnotate(Declaration node) {
224242
}
225243
return null;
226244
}
245+
246+
/// An [Element] processor function type.
247+
/// If `true` is returned, children of [element] will be visited.
248+
typedef bool ElementProcessor(Element element);
249+
250+
/// A [GeneralizingElementVisitor] adapter for [ElementProcessor].
251+
class _ElementVisitorAdapter extends GeneralizingElementVisitor {
252+
final ElementProcessor processor;
253+
_ElementVisitorAdapter(this.processor);
254+
255+
@override
256+
void visitElement(Element element) {
257+
bool visitChildren = processor(element);
258+
if (visitChildren == true) {
259+
element.visitChildren(this);
260+
}
261+
}
262+
}

lib/src/linter.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class DartLinter implements AnalysisErrorListener {
9898
} on Exception catch (e) {
9999
reporter.exception(new LinterException(e.toString()));
100100
}
101-
if (rule._locationInfo != null && !rule._locationInfo.isEmpty) {
101+
if (rule._locationInfo != null && rule._locationInfo.isNotEmpty) {
102102
results.addAll(rule._locationInfo);
103103
rule._locationInfo.clear();
104104
}
@@ -399,7 +399,7 @@ class SourceLinter implements DartLinter, AnalysisErrorListener {
399399
} on Exception catch (e) {
400400
reporter.exception(new LinterException(e.toString()));
401401
}
402-
if (rule._locationInfo != null && !rule._locationInfo.isEmpty) {
402+
if (rule._locationInfo != null && rule._locationInfo.isNotEmpty) {
403403
results.addAll(rule._locationInfo);
404404
rule._locationInfo.clear();
405405
}

lib/src/rules.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'package:linter/src/rules/non_constant_identifier_names.dart';
2020
import 'package:linter/src/rules/one_member_abstracts.dart';
2121
import 'package:linter/src/rules/package_api_docs.dart';
2222
import 'package:linter/src/rules/package_prefixed_library_names.dart';
23+
import 'package:linter/src/rules/prefer_is_not_empty.dart';
2324
import 'package:linter/src/rules/pub/package_names.dart';
2425
import 'package:linter/src/rules/slash_for_doc_comments.dart';
2526
import 'package:linter/src/rules/super_goes_last.dart';
@@ -37,6 +38,7 @@ final Registry ruleRegistry = new Registry()
3738
..register(new LibraryNames())
3839
..register(new LibraryPrefixes())
3940
..register(new NonConstantIdentifierNames())
41+
..register(new PreferIsNotEmpty())
4042
..register(new OneMemberAbstracts())
4143
..register(new PackageApiDocs())
4244
..register(new PackagePrefixedLibraryNames())
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) 2015, 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.prefer_is_not_empty;
6+
7+
import 'package:analyzer/src/generated/ast.dart'
8+
show
9+
AstNode,
10+
AstVisitor,
11+
PrefixExpression,
12+
PrefixedIdentifier,
13+
PropertyAccess,
14+
SimpleAstVisitor,
15+
SimpleIdentifier;
16+
import 'package:analyzer/src/generated/element.dart' show Element;
17+
import 'package:analyzer/src/generated/scanner.dart' show TokenType;
18+
import 'package:linter/src/ast.dart';
19+
import 'package:linter/src/linter.dart';
20+
21+
const desc = 'Use isNotEmpty for Iterables and Maps.';
22+
23+
const details = '''
24+
**PREFER** `x.isNotEmpty` to `!x.isEmpty` for Iterables and Maps.
25+
26+
When testing whether an iterable or map is empty, prefer `isNotEmpty` over
27+
`!isEmpty`.
28+
29+
**GOOD:**
30+
```
31+
if (todo.isNotEmpty) {
32+
sendResults(request, todo.isEmpty);
33+
}
34+
```
35+
36+
**BAD:**
37+
```
38+
if (!sources.isEmpty) {
39+
process(sources);
40+
}
41+
```
42+
''';
43+
44+
class PreferIsNotEmpty extends LintRule {
45+
PreferIsNotEmpty()
46+
: super(
47+
name: 'prefer_is_not_empty',
48+
description: desc,
49+
details: details,
50+
group: Group.style);
51+
52+
@override
53+
AstVisitor getVisitor() => new Visitor(this);
54+
}
55+
56+
class Visitor extends SimpleAstVisitor {
57+
final LintRule rule;
58+
Visitor(this.rule);
59+
60+
@override
61+
visitSimpleIdentifier(SimpleIdentifier identifier) {
62+
AstNode isEmptyAccess = null;
63+
SimpleIdentifier isEmptyIdentifier = null;
64+
65+
AstNode parent = identifier.parent;
66+
if (parent is PropertyAccess) {
67+
isEmptyIdentifier = parent.propertyName;
68+
isEmptyAccess = parent;
69+
} else if (parent is PrefixedIdentifier) {
70+
isEmptyIdentifier = parent.identifier;
71+
isEmptyAccess = parent;
72+
}
73+
74+
if (isEmptyIdentifier == null) {
75+
return;
76+
}
77+
78+
// Should be "isEmpty".
79+
Element propertyElement = isEmptyIdentifier.bestElement;
80+
if (propertyElement == null || 'isEmpty' != propertyElement.name) {
81+
return;
82+
}
83+
// Should have "isNotEmpty".
84+
Element propertyTarget = propertyElement.enclosingElement;
85+
if (propertyTarget == null ||
86+
getChildren(propertyTarget, 'isNotEmpty').isEmpty) {
87+
return;
88+
}
89+
// Should be in PrefixExpression.
90+
if (isEmptyAccess.parent is! PrefixExpression) {
91+
return;
92+
}
93+
PrefixExpression prefixExpression =
94+
isEmptyAccess.parent as PrefixExpression;
95+
// Should be !
96+
if (prefixExpression.operator.type != TokenType.BANG) {
97+
return;
98+
}
99+
100+
rule.reportLint(prefixExpression);
101+
}
102+
}

lib/src/sdk.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class Iterator<E> {
9292
abstract class Iterable<E> {
9393
Iterator<E> get iterator;
9494
bool get isEmpty;
95+
bool get isNotEmpty;
9596
}
9697
9798
abstract class List<E> implements Iterable<E> {
@@ -100,10 +101,14 @@ abstract class List<E> implements Iterable<E> {
100101
void operator []=(int index, E value);
101102
Iterator<E> get iterator => null;
102103
void clear();
104+
bool get isEmpty;
105+
bool get isNotEmpty;
103106
}
104107
105108
abstract class Map<K, V> extends Object {
106109
Iterable<K> get keys;
110+
bool get isEmpty;
111+
bool get isNotEmpty;
107112
}
108113
109114
external bool identical(Object a, Object b);

test/rules/prefer_is_not_empty.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2015, 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+
bool lne = ![].isEmpty; //LINT [12:11]
6+
bool mne = !{}.isEmpty; //LINT
7+
bool ine = !iterable.isEmpty; //LINT
8+
9+
bool le = [].isEmpty;
10+
bool me = {}.isEmpty;
11+
bool ie = iterable.isEmpty;
12+
13+
Iterable get iterable => [];

0 commit comments

Comments
 (0)