|
|
|
/**
|
|
|
|
* @fileoverview Rule to require or disallow yoda comparisons
|
|
|
|
* @author Nicholas C. Zakas
|
|
|
|
* @copyright 2014 Nicholas C. Zakas. All rights reserved.
|
|
|
|
* @copyright 2014 Brandon Mills. All rights reserved.
|
|
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
// Helpers
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether an operator is a comparison operator.
|
|
|
|
* @param {String} operator The operator to check.
|
|
|
|
* @returns {boolean} Whether or not it is a comparison operator.
|
|
|
|
*/
|
|
|
|
function isComparisonOperator(operator) {
|
|
|
|
return (/^(==|===|!=|!==|<|>|<=|>=)$/).test(operator);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether an operator is an equality operator.
|
|
|
|
* @param {String} operator The operator to check.
|
|
|
|
* @returns {boolean} Whether or not it is an equality operator.
|
|
|
|
*/
|
|
|
|
function isEqualityOperator(operator) {
|
|
|
|
return (/^(==|===)$/).test(operator);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether an operator is one used in a range test.
|
|
|
|
* Allowed operators are `<` and `<=`.
|
|
|
|
* @param {String} operator The operator to check.
|
|
|
|
* @returns {boolean} Whether the operator is used in range tests.
|
|
|
|
*/
|
|
|
|
function isRangeTestOperator(operator) {
|
|
|
|
return ["<", "<="].indexOf(operator) >= 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether a non-Literal node is a negative number that should be
|
|
|
|
* treated as if it were a single Literal node.
|
|
|
|
* @param {ASTNode} node Node to test.
|
|
|
|
* @returns {boolean} True if the node is a negative number that looks like a
|
|
|
|
* real literal and should be treated as such.
|
|
|
|
*/
|
|
|
|
function looksLikeLiteral(node) {
|
|
|
|
return (node.type === "UnaryExpression" &&
|
|
|
|
node.operator === "-" &&
|
|
|
|
node.prefix &&
|
|
|
|
node.argument.type === "Literal" &&
|
|
|
|
typeof node.argument.value === "number");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempts to derive a Literal node from nodes that are treated like literals.
|
|
|
|
* @param {ASTNode} node Node to normalize.
|
|
|
|
* @returns {ASTNode} The original node if the node is already a Literal, or a
|
|
|
|
* normalized Literal node with the negative number as the
|
|
|
|
* value if the node represents a negative number literal,
|
|
|
|
* otherwise null if the node cannot be converted to a
|
|
|
|
* normalized literal.
|
|
|
|
*/
|
|
|
|
function getNormalizedLiteral(node) {
|
|
|
|
if (node.type === "Literal") {
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (looksLikeLiteral(node)) {
|
|
|
|
return {
|
|
|
|
type: "Literal",
|
|
|
|
value: -node.argument.value,
|
|
|
|
raw: "-" + node.argument.value
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether two expressions reference the same value. For example:
|
|
|
|
* a = a
|
|
|
|
* a.b = a.b
|
|
|
|
* a[0] = a[0]
|
|
|
|
* a['b'] = a['b']
|
|
|
|
* @param {ASTNode} a Left side of the comparison.
|
|
|
|
* @param {ASTNode} b Right side of the comparison.
|
|
|
|
* @returns {boolean} True if both sides match and reference the same value.
|
|
|
|
*/
|
|
|
|
function same(a, b) {
|
|
|
|
if (a.type !== b.type) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (a.type) {
|
|
|
|
case "Identifier":
|
|
|
|
return a.name === b.name;
|
|
|
|
case "Literal":
|
|
|
|
return a.value === b.value;
|
|
|
|
case "MemberExpression":
|
|
|
|
// x[0] = x[0]
|
|
|
|
// x[y] = x[y]
|
|
|
|
// x.y = x.y
|
|
|
|
return same(a.object, b.object) && same(a.property, b.property);
|
|
|
|
case "ThisExpression":
|
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Rule Definition
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
module.exports = function(context) {
|
|
|
|
|
|
|
|
// Default to "never" (!always) if no option
|
|
|
|
var always = (context.options[0] === "always");
|
|
|
|
var exceptRange = (context.options[1] && context.options[1].exceptRange);
|
|
|
|
var onlyEquality = (context.options[1] && context.options[1].onlyEquality);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether node represents a range test.
|
|
|
|
* A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
|
|
|
|
* test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
|
|
|
|
* both operators must be `<` or `<=`. Finally, the literal on the left side
|
|
|
|
* must be less than or equal to the literal on the right side so that the
|
|
|
|
* test makes any sense.
|
|
|
|
* @param {ASTNode} node LogicalExpression node to test.
|
|
|
|
* @returns {Boolean} Whether node is a range test.
|
|
|
|
*/
|
|
|
|
function isRangeTest(node) {
|
|
|
|
var left = node.left,
|
|
|
|
right = node.right;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether node is of the form `0 <= x && x < 1`.
|
|
|
|
* @returns {Boolean} Whether node is a "between" range test.
|
|
|
|
*/
|
|
|
|
function isBetweenTest() {
|
|
|
|
var leftLiteral, rightLiteral;
|
|
|
|
|
|
|
|
return (node.operator === "&&" &&
|
|
|
|
(leftLiteral = getNormalizedLiteral(left.left)) &&
|
|
|
|
(rightLiteral = getNormalizedLiteral(right.right)) &&
|
|
|
|
leftLiteral.value <= rightLiteral.value &&
|
|
|
|
same(left.right, right.left));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether node is of the form `x < 0 || 1 <= x`.
|
|
|
|
* @returns {Boolean} Whether node is an "outside" range test.
|
|
|
|
*/
|
|
|
|
function isOutsideTest() {
|
|
|
|
var leftLiteral, rightLiteral;
|
|
|
|
|
|
|
|
return (node.operator === "||" &&
|
|
|
|
(leftLiteral = getNormalizedLiteral(left.right)) &&
|
|
|
|
(rightLiteral = getNormalizedLiteral(right.left)) &&
|
|
|
|
leftLiteral.value <= rightLiteral.value &&
|
|
|
|
same(left.left, right.right));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether node is wrapped in parentheses.
|
|
|
|
* @returns {Boolean} Whether node is preceded immediately by an open
|
|
|
|
* paren token and followed immediately by a close
|
|
|
|
* paren token.
|
|
|
|
*/
|
|
|
|
function isParenWrapped() {
|
|
|
|
var tokenBefore, tokenAfter;
|
|
|
|
|
|
|
|
return ((tokenBefore = context.getTokenBefore(node)) &&
|
|
|
|
tokenBefore.value === "(" &&
|
|
|
|
(tokenAfter = context.getTokenAfter(node)) &&
|
|
|
|
tokenAfter.value === ")");
|
|
|
|
}
|
|
|
|
|
|
|
|
return (node.type === "LogicalExpression" &&
|
|
|
|
left.type === "BinaryExpression" &&
|
|
|
|
right.type === "BinaryExpression" &&
|
|
|
|
isRangeTestOperator(left.operator) &&
|
|
|
|
isRangeTestOperator(right.operator) &&
|
|
|
|
(isBetweenTest() || isOutsideTest()) &&
|
|
|
|
isParenWrapped());
|
|
|
|
}
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
// Public
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
|
|
|
|
return {
|
|
|
|
"BinaryExpression": always ? function(node) {
|
|
|
|
|
|
|
|
// Comparisons must always be yoda-style: if ("blue" === color)
|
|
|
|
if (
|
|
|
|
(node.right.type === "Literal" || looksLikeLiteral(node.right)) &&
|
|
|
|
!(node.left.type === "Literal" || looksLikeLiteral(node.left)) &&
|
|
|
|
!(!isEqualityOperator(node.operator) && onlyEquality) &&
|
|
|
|
isComparisonOperator(node.operator) &&
|
|
|
|
!(exceptRange && isRangeTest(context.getAncestors().pop()))
|
|
|
|
) {
|
|
|
|
context.report(node, "Expected literal to be on the left side of " + node.operator + ".");
|
|
|
|
}
|
|
|
|
|
|
|
|
} : function(node) {
|
|
|
|
|
|
|
|
// Comparisons must never be yoda-style (default)
|
|
|
|
if (
|
|
|
|
(node.left.type === "Literal" || looksLikeLiteral(node.left)) &&
|
|
|
|
!(node.right.type === "Literal" || looksLikeLiteral(node.right)) &&
|
|
|
|
!(!isEqualityOperator(node.operator) && onlyEquality) &&
|
|
|
|
isComparisonOperator(node.operator) &&
|
|
|
|
!(exceptRange && isRangeTest(context.getAncestors().pop()))
|
|
|
|
) {
|
|
|
|
context.report(node, "Expected literal to be on the right side of " + node.operator + ".");
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports.schema = [
|
|
|
|
{
|
|
|
|
"enum": ["always", "never"]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"type": "object",
|
|
|
|
"properties": {
|
|
|
|
"exceptRange": {
|
|
|
|
"type": "boolean"
|
|
|
|
},
|
|
|
|
"onlyEquality": {
|
|
|
|
"type": "boolean"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"additionalProperties": false
|
|
|
|
}
|
|
|
|
];
|