blob: 0c1706a96a38a695f16411ea995e07788397073a [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/type.dart';
import '../analyzer.dart';
import '../ast.dart';
import '../extensions.dart';
const _desc = r'Use `isEmpty` for Iterables and Maps.';
const _details = r'''
**DON'T** use `length` to see if a collection is empty.
The `Iterable` contract does not require that a collection know its length or be
able to provide it in constant time. Calling `length` just to see if the
collection contains anything can be painfully slow.
Instead, there are faster and more readable getters: `isEmpty` and
`isNotEmpty`. Use the one that doesn't require you to negate the result.
**BAD:**
```dart
if (lunchBox.length == 0) return 'so hungry...';
if (words.length != 0) return words.join(' ');
```
**GOOD:**
```dart
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
```
''';
class PreferIsEmpty extends LintRule {
// TODO(brianwilkerson) Both `alwaysFalse` and `alwaysTrue` should be warnings
// rather than lints because they represent a bug rather than a style
// preference.
static const LintCode alwaysFalse = LintCode(
'prefer_is_empty',
"The comparison is always 'false' because the length is always greater "
'than or equal to 0.');
static const LintCode alwaysTrue = LintCode(
'prefer_is_empty',
"The comparison is always 'true' because the length is always greater "
'than or equal to 0.');
static const LintCode useIsEmpty = LintCode(
'prefer_is_empty',
"Use 'isEmpty' instead of 'length' to test whether the collection is "
'empty.',
correctionMessage: "Try rewriting the expression to use 'isEmpty'.");
static const LintCode useIsNotEmpty = LintCode(
'prefer_is_empty',
"Use 'isNotEmpty' instead of 'length' to test whether the collection is "
'empty.',
correctionMessage: "Try rewriting the expression to use 'isNotEmpty'.");
PreferIsEmpty()
: super(
name: 'prefer_is_empty',
description: _desc,
details: _details,
group: Group.style);
@override
List<LintCode> get lintCodes =>
[alwaysFalse, alwaysTrue, useIsEmpty, useIsNotEmpty];
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this, context);
registry.addBinaryExpression(this, visitor);
}
}
class _Visitor extends SimpleAstVisitor<void> {
final PreferIsEmpty rule;
final LinterContext context;
_Visitor(this.rule, this.context);
@override
void visitBinaryExpression(BinaryExpression node) {
// todo(pq): not evaluating constants deliberately but we *should*.
// see: https://github.com/dart-lang/linter/issues/2818
var value = getIntValue(node.rightOperand, null);
if (value != null) {
if (_isLengthAccess(node.leftOperand)) {
_check(node, value, constantOnRight: true);
}
} else {
value = getIntValue(node.leftOperand, null);
// ignore: invariant_booleans
if (value != null) {
if (_isLengthAccess(node.rightOperand)) {
_check(node, value, constantOnRight: false);
}
}
}
}
void _check(BinaryExpression expression, int value,
{required bool constantOnRight}) {
// Don't lint if we're in a const constructor initializer.
var constructorInitializer =
expression.thisOrAncestorOfType<ConstructorInitializer>();
if (constructorInitializer != null) {
var constructorDecl = constructorInitializer.parent;
if (constructorDecl is! ConstructorDeclaration ||
constructorDecl.constKeyword != null) {
return;
}
}
// Or in a const context.
// See: https://github.com/dart-lang/linter/issues/1719
if (expression.inConstantContext) {
return;
}
var operator = expression.operator;
if (value == 0) {
if (operator.type == TokenType.EQ_EQ ||
operator.type == TokenType.LT_EQ) {
rule.reportLint(expression, errorCode: PreferIsEmpty.useIsEmpty);
} else if (operator.type == TokenType.GT ||
operator.type == TokenType.BANG_EQ) {
rule.reportLint(expression, errorCode: PreferIsEmpty.useIsNotEmpty);
} else if (operator.type == TokenType.LT) {
rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysFalse);
} else if (operator.type == TokenType.GT_EQ) {
rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysTrue);
}
} else if (value == 1) {
if (constantOnRight) {
// 'length >= 1' is same as 'isNotEmpty',
// and 'length < 1' is same as 'isEmpty'
if (operator.type == TokenType.GT_EQ) {
rule.reportLint(expression, errorCode: PreferIsEmpty.useIsNotEmpty);
} else if (operator.type == TokenType.LT) {
rule.reportLint(expression, errorCode: PreferIsEmpty.useIsEmpty);
}
} else {
// '1 <= length' is same as 'isNotEmpty',
// and '1 > length' is same as 'isEmpty'
if (operator.type == TokenType.LT_EQ) {
rule.reportLint(expression, errorCode: PreferIsEmpty.useIsNotEmpty);
} else if (operator.type == TokenType.GT) {
rule.reportLint(expression, errorCode: PreferIsEmpty.useIsEmpty);
}
}
} else if (value < 0) {
if (constantOnRight) {
// 'length' is always >= 0, so comparing with negative makes no sense.
if (operator.type == TokenType.EQ_EQ ||
operator.type == TokenType.LT_EQ ||
operator.type == TokenType.LT) {
rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysFalse);
} else if (operator.type == TokenType.BANG_EQ ||
operator.type == TokenType.GT_EQ ||
operator.type == TokenType.GT) {
rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysTrue);
}
} else {
// 'length' is always >= 0, so comparing with negative makes no sense.
if (operator.type == TokenType.EQ_EQ ||
operator.type == TokenType.GT_EQ ||
operator.type == TokenType.GT) {
rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysFalse);
} else if (operator.type == TokenType.BANG_EQ ||
operator.type == TokenType.LT_EQ ||
operator.type == TokenType.LT) {
rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysTrue);
}
}
}
}
/// todo(pq): consider sharing
T? _drillDownTo<T extends Expression>(Expression expression,
{required bool ignoreParens, required bool ignoreAs}) {
var search = expression;
// ignore: literal_only_boolean_expressions
while (true) {
if (ignoreParens && search is ParenthesizedExpression) {
search = search.expression;
} else if (ignoreAs && search is AsExpression) {
search = search.expression;
} else {
break;
}
}
return search is T ? search : null;
}
bool _isLengthAccess(Expression operand) {
var node = _drillDownTo(operand, ignoreParens: true, ignoreAs: true);
if (node == null) {
return false;
}
SimpleIdentifier? identifier;
InterfaceType? type;
if (node is PrefixedIdentifier) {
identifier = node.identifier;
var operandType = node.prefix.staticType;
if (operandType is InterfaceType) {
type = operandType;
}
} else if (node is PropertyAccess) {
identifier = node.propertyName;
var parentType = node.target?.staticType;
if (parentType is InterfaceType) {
type = parentType;
}
}
if (identifier?.name != 'length') {
return false;
}
// Should be subtype of Iterable, Map or String.
if (type == null ||
!type.implementsInterface('Iterable', 'dart.core') &&
!type.implementsInterface('Map', 'dart.core') &&
!type.isDartCoreString) {
return false;
}
return true;
}
}