| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 | /** * @fileoverview Rule to require or disallow yoda comparisons * @author Nicholas C. Zakas */"use strict";//--------------------------------------------------------------------------// Requirements//--------------------------------------------------------------------------const astUtils = require("../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: {        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))                    });                }            }        };    }};
 |