123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- /**
- * @fileoverview Rule to require or disallow yoda comparisons
- * @author Nicholas C. Zakas
- */
- "use strict";
-
- //--------------------------------------------------------------------------
- // Requirements
- //--------------------------------------------------------------------------
-
- const astUtils = require("../util/ast-utils");
-
- //--------------------------------------------------------------------------
- // 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.
- * @param {number} [defaultValue] The default value to be returned if the node
- * is not a Literal.
- * @returns {ASTNode} One of the following options.
- * 1. The original node if the node is already a Literal
- * 2. A normalized Literal node with the negative number as the value if the
- * node represents a negative number literal.
- * 3. The Literal node which has the `defaultValue` argument if it exists.
- * 4. Otherwise `null`.
- */
- function getNormalizedLiteral(node, defaultValue) {
- if (node.type === "Literal") {
- return node;
- }
-
- if (looksLikeLiteral(node)) {
- return {
- type: "Literal",
- value: -node.argument.value,
- raw: `-${node.argument.value}`
- };
- }
-
- if (defaultValue) {
- return {
- type: "Literal",
- value: defaultValue,
- raw: String(defaultValue)
- };
- }
-
- 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": {
- const nameA = astUtils.getStaticPropertyName(a);
-
- // x.y = x["y"]
- if (nameA) {
- return (
- same(a.object, b.object) &&
- nameA === astUtils.getStaticPropertyName(b)
- );
- }
-
- /*
- * x[0] = x[0]
- * x[y] = x[y]
- * x.y = x.y
- */
- return (
- a.computed === b.computed &&
- same(a.object, b.object) &&
- same(a.property, b.property)
- );
- }
-
- case "ThisExpression":
- return true;
-
- default:
- return false;
- }
- }
-
- //------------------------------------------------------------------------------
- // Rule Definition
- //------------------------------------------------------------------------------
-
- module.exports = {
- meta: {
- type: "suggestion",
-
- docs: {
- description: "require or disallow \"Yoda\" conditions",
- category: "Best Practices",
- recommended: false,
- url: "https://eslint.org/docs/rules/yoda"
- },
-
- schema: [
- {
- enum: ["always", "never"]
- },
- {
- type: "object",
- properties: {
- exceptRange: {
- type: "boolean"
- },
- onlyEquality: {
- type: "boolean"
- }
- },
- additionalProperties: false
- }
- ],
-
- fixable: "code"
- },
-
- create(context) {
-
- // Default to "never" (!always) if no option
- const always = (context.options[0] === "always");
- const exceptRange = (context.options[1] && context.options[1].exceptRange);
- const onlyEquality = (context.options[1] && context.options[1].onlyEquality);
-
- const sourceCode = context.getSourceCode();
-
- /**
- * 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) {
- const 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() {
- let leftLiteral, rightLiteral;
-
- return (node.operator === "&&" &&
- (leftLiteral = getNormalizedLiteral(left.left)) &&
- (rightLiteral = getNormalizedLiteral(right.right, Number.POSITIVE_INFINITY)) &&
- 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() {
- let leftLiteral, rightLiteral;
-
- return (node.operator === "||" &&
- (leftLiteral = getNormalizedLiteral(left.right, Number.NEGATIVE_INFINITY)) &&
- (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() {
- return astUtils.isParenthesised(sourceCode, node);
- }
-
- return (node.type === "LogicalExpression" &&
- left.type === "BinaryExpression" &&
- right.type === "BinaryExpression" &&
- isRangeTestOperator(left.operator) &&
- isRangeTestOperator(right.operator) &&
- (isBetweenTest() || isOutsideTest()) &&
- isParenWrapped());
- }
-
- const OPERATOR_FLIP_MAP = {
- "===": "===",
- "!==": "!==",
- "==": "==",
- "!=": "!=",
- "<": ">",
- ">": "<",
- "<=": ">=",
- ">=": "<="
- };
-
- /**
- * Returns a string representation of a BinaryExpression node with its sides/operator flipped around.
- * @param {ASTNode} node The BinaryExpression node
- * @returns {string} A string representation of the node with the sides and operator flipped
- */
- function getFlippedString(node) {
- const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
- const textBeforeOperator = sourceCode.getText().slice(sourceCode.getTokenBefore(operatorToken).range[1], operatorToken.range[0]);
- const textAfterOperator = sourceCode.getText().slice(operatorToken.range[1], sourceCode.getTokenAfter(operatorToken).range[0]);
- const leftText = sourceCode.getText().slice(node.range[0], sourceCode.getTokenBefore(operatorToken).range[1]);
- const rightText = sourceCode.getText().slice(sourceCode.getTokenAfter(operatorToken).range[0], node.range[1]);
-
- return rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText;
- }
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- return {
- BinaryExpression(node) {
- const expectedLiteral = always ? node.left : node.right;
- const expectedNonLiteral = always ? node.right : node.left;
-
- // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
- if (
- (expectedNonLiteral.type === "Literal" || looksLikeLiteral(expectedNonLiteral)) &&
- !(expectedLiteral.type === "Literal" || looksLikeLiteral(expectedLiteral)) &&
- !(!isEqualityOperator(node.operator) && onlyEquality) &&
- isComparisonOperator(node.operator) &&
- !(exceptRange && isRangeTest(context.getAncestors().pop()))
- ) {
- context.report({
- node,
- message: "Expected literal to be on the {{expectedSide}} side of {{operator}}.",
- data: {
- operator: node.operator,
- expectedSide: always ? "left" : "right"
- },
- fix: fixer => fixer.replaceText(node, getFlippedString(node))
- });
- }
-
- }
- };
-
- }
- };
|