--- /dev/null
+/**
+ * @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 isNegativeNumericLiteral(node) {
+ return (
+ node.type === "UnaryExpression" &&
+ node.operator === "-" &&
+ node.prefix &&
+ astUtils.isNumericLiteral(node.argument)
+ );
+}
+
+/**
+ * Determines whether a node is a Template Literal which can be determined statically.
+ * @param {ASTNode} node Node to test
+ * @returns {boolean} True if the node is a Template Literal without expression.
+ */
+function isStaticTemplateLiteral(node) {
+ return node.type === "TemplateLiteral" && node.expressions.length === 0;
+}
+
+/**
+ * Determines whether a non-Literal node should be treated as a single Literal node.
+ * @param {ASTNode} node Node to test
+ * @returns {boolean} True if the node should be treated as a single Literal node.
+ */
+function looksLikeLiteral(node) {
+ return isNegativeNumericLiteral(node) || isStaticTemplateLiteral(node);
+}
+
+/**
+ * Attempts to derive a Literal node from nodes that are treated like literals.
+ * @param {ASTNode} node Node to normalize.
+ * @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. A normalized Literal node with the string as the value if the node is
+ * a Template Literal without expression.
+ * 4. Otherwise `null`.
+ */
+function getNormalizedLiteral(node) {
+ if (node.type === "Literal") {
+ return node;
+ }
+
+ if (isNegativeNumericLiteral(node)) {
+ return {
+ type: "Literal",
+ value: -node.argument.value,
+ raw: `-${node.argument.value}`
+ };
+ }
+
+ if (isStaticTemplateLiteral(node)) {
+ return {
+ type: "Literal",
+ value: node.quasis[0].value.cooked,
+ raw: node.quasis[0].value.raw
+ };
+ }
+
+ return null;
+}
+
+//------------------------------------------------------------------------------
+// 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() {
+ if (node.operator === "&&" && astUtils.isSameReference(left.right, right.left)) {
+ const leftLiteral = getNormalizedLiteral(left.left);
+ const rightLiteral = getNormalizedLiteral(right.right);
+
+ if (leftLiteral === null && rightLiteral === null) {
+ return false;
+ }
+
+ if (rightLiteral === null || leftLiteral === null) {
+ return true;
+ }
+
+ if (leftLiteral.value <= rightLiteral.value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether node is of the form `x < 0 || 1 <= x`.
+ * @returns {boolean} Whether node is an "outside" range test.
+ */
+ function isOutsideTest() {
+ if (node.operator === "||" && astUtils.isSameReference(left.left, right.right)) {
+ const leftLiteral = getNormalizedLiteral(left.right);
+ const rightLiteral = getNormalizedLiteral(right.left);
+
+ if (leftLiteral === null && rightLiteral === null) {
+ return false;
+ }
+
+ if (rightLiteral === null || leftLiteral === null) {
+ return true;
+ }
+
+ if (leftLiteral.value <= rightLiteral.value) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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 lastLeftToken = sourceCode.getTokenBefore(operatorToken);
+ const firstRightToken = sourceCode.getTokenAfter(operatorToken);
+
+ const source = sourceCode.getText();
+
+ const leftText = source.slice(
+ node.range[0],
+ lastLeftToken.range[1]
+ );
+ const textBeforeOperator = source.slice(
+ lastLeftToken.range[1],
+ operatorToken.range[0]
+ );
+ const textAfterOperator = source.slice(
+ operatorToken.range[1],
+ firstRightToken.range[0]
+ );
+ const rightText = source.slice(
+ firstRightToken.range[0],
+ node.range[1]
+ );
+
+ const tokenBefore = sourceCode.getTokenBefore(node);
+ const tokenAfter = sourceCode.getTokenAfter(node);
+ let prefix = "";
+ let suffix = "";
+
+ if (
+ tokenBefore &&
+ tokenBefore.range[1] === node.range[0] &&
+ !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)
+ ) {
+ prefix = " ";
+ }
+
+ if (
+ tokenAfter &&
+ node.range[1] === tokenAfter.range[0] &&
+ !astUtils.canTokensBeAdjacent(lastLeftToken, tokenAfter)
+ ) {
+ suffix = " ";
+ }
+
+ return (
+ prefix +
+ rightText +
+ textBeforeOperator +
+ OPERATOR_FLIP_MAP[operatorToken.value] +
+ textAfterOperator +
+ leftText +
+ suffix
+ );
+ }
+
+ //--------------------------------------------------------------------------
+ // 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))
+ });
+ }
+ }
+ };
+ }
+};