You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							327 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
	
	
							327 lines
						
					
					
						
							11 KiB
						
					
					
				/** | 
						|
 * @fileoverview Rule to require or disallow yoda comparisons | 
						|
 * @author Nicholas C. Zakas | 
						|
 */ | 
						|
"use strict"; | 
						|
 | 
						|
//-------------------------------------------------------------------------- | 
						|
// Requirements | 
						|
//-------------------------------------------------------------------------- | 
						|
 | 
						|
const astUtils = require("./utils/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 (/^(==|===|!=|!==|<|>|<=|>=)$/u).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 (/^(==|===)$/u).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 !== null) { | 
						|
                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", | 
						|
                        default: false | 
						|
                    }, | 
						|
                    onlyEquality: { | 
						|
                        type: "boolean", | 
						|
                        default: false | 
						|
                    } | 
						|
                }, | 
						|
                additionalProperties: false | 
						|
            } | 
						|
        ], | 
						|
 | 
						|
        fixable: "code", | 
						|
        messages: { | 
						|
            expected: "Expected literal to be on the {{expectedSide}} side of {{operator}}." | 
						|
        } | 
						|
    }, | 
						|
 | 
						|
    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 tokenBefore = sourceCode.getTokenBefore(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 firstRightToken = sourceCode.getTokenAfter(operatorToken); | 
						|
            const rightText = sourceCode.getText().slice(firstRightToken.range[0], node.range[1]); | 
						|
 | 
						|
            let prefix = ""; | 
						|
 | 
						|
            if (tokenBefore && tokenBefore.range[1] === node.range[0] && | 
						|
                    !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)) { | 
						|
                prefix = " "; | 
						|
            } | 
						|
 | 
						|
            return prefix + 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, | 
						|
                        messageId: "expected", | 
						|
                        data: { | 
						|
                            operator: node.operator, | 
						|
                            expectedSide: always ? "left" : "right" | 
						|
                        }, | 
						|
                        fix: fixer => fixer.replaceText(node, getFlippedString(node)) | 
						|
                    }); | 
						|
                } | 
						|
 | 
						|
            } | 
						|
        }; | 
						|
 | 
						|
    } | 
						|
};
 | 
						|
 |