Permalink
Cannot retrieve contributors at this time
| /** | |
| * @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)) | |
| }); | |
| } | |
| } | |
| }; | |
| } | |
| }; |