[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to require or disallow yoda comparisons
|
---|
| 3 | * @author Nicholas C. Zakas
|
---|
| 4 | */
|
---|
| 5 | "use strict";
|
---|
| 6 |
|
---|
| 7 | //--------------------------------------------------------------------------
|
---|
| 8 | // Requirements
|
---|
| 9 | //--------------------------------------------------------------------------
|
---|
| 10 |
|
---|
| 11 | const astUtils = require("./utils/ast-utils");
|
---|
| 12 |
|
---|
| 13 | //--------------------------------------------------------------------------
|
---|
| 14 | // Helpers
|
---|
| 15 | //--------------------------------------------------------------------------
|
---|
| 16 |
|
---|
| 17 | /**
|
---|
| 18 | * Determines whether an operator is a comparison operator.
|
---|
| 19 | * @param {string} operator The operator to check.
|
---|
| 20 | * @returns {boolean} Whether or not it is a comparison operator.
|
---|
| 21 | */
|
---|
| 22 | function isComparisonOperator(operator) {
|
---|
| 23 | return /^(==|===|!=|!==|<|>|<=|>=)$/u.test(operator);
|
---|
| 24 | }
|
---|
| 25 |
|
---|
| 26 | /**
|
---|
| 27 | * Determines whether an operator is an equality operator.
|
---|
| 28 | * @param {string} operator The operator to check.
|
---|
| 29 | * @returns {boolean} Whether or not it is an equality operator.
|
---|
| 30 | */
|
---|
| 31 | function isEqualityOperator(operator) {
|
---|
| 32 | return /^(==|===)$/u.test(operator);
|
---|
| 33 | }
|
---|
| 34 |
|
---|
| 35 | /**
|
---|
| 36 | * Determines whether an operator is one used in a range test.
|
---|
| 37 | * Allowed operators are `<` and `<=`.
|
---|
| 38 | * @param {string} operator The operator to check.
|
---|
| 39 | * @returns {boolean} Whether the operator is used in range tests.
|
---|
| 40 | */
|
---|
| 41 | function isRangeTestOperator(operator) {
|
---|
| 42 | return ["<", "<="].includes(operator);
|
---|
| 43 | }
|
---|
| 44 |
|
---|
| 45 | /**
|
---|
| 46 | * Determines whether a non-Literal node is a negative number that should be
|
---|
| 47 | * treated as if it were a single Literal node.
|
---|
| 48 | * @param {ASTNode} node Node to test.
|
---|
| 49 | * @returns {boolean} True if the node is a negative number that looks like a
|
---|
| 50 | * real literal and should be treated as such.
|
---|
| 51 | */
|
---|
| 52 | function isNegativeNumericLiteral(node) {
|
---|
| 53 | return (
|
---|
| 54 | node.type === "UnaryExpression" &&
|
---|
| 55 | node.operator === "-" &&
|
---|
| 56 | node.prefix &&
|
---|
| 57 | astUtils.isNumericLiteral(node.argument)
|
---|
| 58 | );
|
---|
| 59 | }
|
---|
| 60 |
|
---|
| 61 | /**
|
---|
| 62 | * Determines whether a non-Literal node should be treated as a single Literal node.
|
---|
| 63 | * @param {ASTNode} node Node to test
|
---|
| 64 | * @returns {boolean} True if the node should be treated as a single Literal node.
|
---|
| 65 | */
|
---|
| 66 | function looksLikeLiteral(node) {
|
---|
| 67 | return isNegativeNumericLiteral(node) || astUtils.isStaticTemplateLiteral(node);
|
---|
| 68 | }
|
---|
| 69 |
|
---|
| 70 | /**
|
---|
| 71 | * Attempts to derive a Literal node from nodes that are treated like literals.
|
---|
| 72 | * @param {ASTNode} node Node to normalize.
|
---|
| 73 | * @returns {ASTNode} One of the following options.
|
---|
| 74 | * 1. The original node if the node is already a Literal
|
---|
| 75 | * 2. A normalized Literal node with the negative number as the value if the
|
---|
| 76 | * node represents a negative number literal.
|
---|
| 77 | * 3. A normalized Literal node with the string as the value if the node is
|
---|
| 78 | * a Template Literal without expression.
|
---|
| 79 | * 4. Otherwise `null`.
|
---|
| 80 | */
|
---|
| 81 | function getNormalizedLiteral(node) {
|
---|
| 82 | if (node.type === "Literal") {
|
---|
| 83 | return node;
|
---|
| 84 | }
|
---|
| 85 |
|
---|
| 86 | if (isNegativeNumericLiteral(node)) {
|
---|
| 87 | return {
|
---|
| 88 | type: "Literal",
|
---|
| 89 | value: -node.argument.value,
|
---|
| 90 | raw: `-${node.argument.value}`
|
---|
| 91 | };
|
---|
| 92 | }
|
---|
| 93 |
|
---|
| 94 | if (astUtils.isStaticTemplateLiteral(node)) {
|
---|
| 95 | return {
|
---|
| 96 | type: "Literal",
|
---|
| 97 | value: node.quasis[0].value.cooked,
|
---|
| 98 | raw: node.quasis[0].value.raw
|
---|
| 99 | };
|
---|
| 100 | }
|
---|
| 101 |
|
---|
| 102 | return null;
|
---|
| 103 | }
|
---|
| 104 |
|
---|
| 105 | //------------------------------------------------------------------------------
|
---|
| 106 | // Rule Definition
|
---|
| 107 | //------------------------------------------------------------------------------
|
---|
| 108 |
|
---|
| 109 | /** @type {import('../shared/types').Rule} */
|
---|
| 110 | module.exports = {
|
---|
| 111 | meta: {
|
---|
| 112 | type: "suggestion",
|
---|
| 113 |
|
---|
| 114 | docs: {
|
---|
| 115 | description: 'Require or disallow "Yoda" conditions',
|
---|
| 116 | recommended: false,
|
---|
| 117 | url: "https://eslint.org/docs/latest/rules/yoda"
|
---|
| 118 | },
|
---|
| 119 |
|
---|
| 120 | schema: [
|
---|
| 121 | {
|
---|
| 122 | enum: ["always", "never"]
|
---|
| 123 | },
|
---|
| 124 | {
|
---|
| 125 | type: "object",
|
---|
| 126 | properties: {
|
---|
| 127 | exceptRange: {
|
---|
| 128 | type: "boolean",
|
---|
| 129 | default: false
|
---|
| 130 | },
|
---|
| 131 | onlyEquality: {
|
---|
| 132 | type: "boolean",
|
---|
| 133 | default: false
|
---|
| 134 | }
|
---|
| 135 | },
|
---|
| 136 | additionalProperties: false
|
---|
| 137 | }
|
---|
| 138 | ],
|
---|
| 139 |
|
---|
| 140 | fixable: "code",
|
---|
| 141 | messages: {
|
---|
| 142 | expected:
|
---|
| 143 | "Expected literal to be on the {{expectedSide}} side of {{operator}}."
|
---|
| 144 | }
|
---|
| 145 | },
|
---|
| 146 |
|
---|
| 147 | create(context) {
|
---|
| 148 |
|
---|
| 149 | // Default to "never" (!always) if no option
|
---|
| 150 | const always = context.options[0] === "always";
|
---|
| 151 | const exceptRange =
|
---|
| 152 | context.options[1] && context.options[1].exceptRange;
|
---|
| 153 | const onlyEquality =
|
---|
| 154 | context.options[1] && context.options[1].onlyEquality;
|
---|
| 155 |
|
---|
| 156 | const sourceCode = context.sourceCode;
|
---|
| 157 |
|
---|
| 158 | /**
|
---|
| 159 | * Determines whether node represents a range test.
|
---|
| 160 | * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
|
---|
| 161 | * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
|
---|
| 162 | * both operators must be `<` or `<=`. Finally, the literal on the left side
|
---|
| 163 | * must be less than or equal to the literal on the right side so that the
|
---|
| 164 | * test makes any sense.
|
---|
| 165 | * @param {ASTNode} node LogicalExpression node to test.
|
---|
| 166 | * @returns {boolean} Whether node is a range test.
|
---|
| 167 | */
|
---|
| 168 | function isRangeTest(node) {
|
---|
| 169 | const left = node.left,
|
---|
| 170 | right = node.right;
|
---|
| 171 |
|
---|
| 172 | /**
|
---|
| 173 | * Determines whether node is of the form `0 <= x && x < 1`.
|
---|
| 174 | * @returns {boolean} Whether node is a "between" range test.
|
---|
| 175 | */
|
---|
| 176 | function isBetweenTest() {
|
---|
| 177 | if (node.operator === "&&" && astUtils.isSameReference(left.right, right.left)) {
|
---|
| 178 | const leftLiteral = getNormalizedLiteral(left.left);
|
---|
| 179 | const rightLiteral = getNormalizedLiteral(right.right);
|
---|
| 180 |
|
---|
| 181 | if (leftLiteral === null && rightLiteral === null) {
|
---|
| 182 | return false;
|
---|
| 183 | }
|
---|
| 184 |
|
---|
| 185 | if (rightLiteral === null || leftLiteral === null) {
|
---|
| 186 | return true;
|
---|
| 187 | }
|
---|
| 188 |
|
---|
| 189 | if (leftLiteral.value <= rightLiteral.value) {
|
---|
| 190 | return true;
|
---|
| 191 | }
|
---|
| 192 | }
|
---|
| 193 | return false;
|
---|
| 194 | }
|
---|
| 195 |
|
---|
| 196 | /**
|
---|
| 197 | * Determines whether node is of the form `x < 0 || 1 <= x`.
|
---|
| 198 | * @returns {boolean} Whether node is an "outside" range test.
|
---|
| 199 | */
|
---|
| 200 | function isOutsideTest() {
|
---|
| 201 | if (node.operator === "||" && astUtils.isSameReference(left.left, right.right)) {
|
---|
| 202 | const leftLiteral = getNormalizedLiteral(left.right);
|
---|
| 203 | const rightLiteral = getNormalizedLiteral(right.left);
|
---|
| 204 |
|
---|
| 205 | if (leftLiteral === null && rightLiteral === null) {
|
---|
| 206 | return false;
|
---|
| 207 | }
|
---|
| 208 |
|
---|
| 209 | if (rightLiteral === null || leftLiteral === null) {
|
---|
| 210 | return true;
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | if (leftLiteral.value <= rightLiteral.value) {
|
---|
| 214 | return true;
|
---|
| 215 | }
|
---|
| 216 | }
|
---|
| 217 |
|
---|
| 218 | return false;
|
---|
| 219 | }
|
---|
| 220 |
|
---|
| 221 | /**
|
---|
| 222 | * Determines whether node is wrapped in parentheses.
|
---|
| 223 | * @returns {boolean} Whether node is preceded immediately by an open
|
---|
| 224 | * paren token and followed immediately by a close
|
---|
| 225 | * paren token.
|
---|
| 226 | */
|
---|
| 227 | function isParenWrapped() {
|
---|
| 228 | return astUtils.isParenthesised(sourceCode, node);
|
---|
| 229 | }
|
---|
| 230 |
|
---|
| 231 | return (
|
---|
| 232 | node.type === "LogicalExpression" &&
|
---|
| 233 | left.type === "BinaryExpression" &&
|
---|
| 234 | right.type === "BinaryExpression" &&
|
---|
| 235 | isRangeTestOperator(left.operator) &&
|
---|
| 236 | isRangeTestOperator(right.operator) &&
|
---|
| 237 | (isBetweenTest() || isOutsideTest()) &&
|
---|
| 238 | isParenWrapped()
|
---|
| 239 | );
|
---|
| 240 | }
|
---|
| 241 |
|
---|
| 242 | const OPERATOR_FLIP_MAP = {
|
---|
| 243 | "===": "===",
|
---|
| 244 | "!==": "!==",
|
---|
| 245 | "==": "==",
|
---|
| 246 | "!=": "!=",
|
---|
| 247 | "<": ">",
|
---|
| 248 | ">": "<",
|
---|
| 249 | "<=": ">=",
|
---|
| 250 | ">=": "<="
|
---|
| 251 | };
|
---|
| 252 |
|
---|
| 253 | /**
|
---|
| 254 | * Returns a string representation of a BinaryExpression node with its sides/operator flipped around.
|
---|
| 255 | * @param {ASTNode} node The BinaryExpression node
|
---|
| 256 | * @returns {string} A string representation of the node with the sides and operator flipped
|
---|
| 257 | */
|
---|
| 258 | function getFlippedString(node) {
|
---|
| 259 | const operatorToken = sourceCode.getFirstTokenBetween(
|
---|
| 260 | node.left,
|
---|
| 261 | node.right,
|
---|
| 262 | token => token.value === node.operator
|
---|
| 263 | );
|
---|
| 264 | const lastLeftToken = sourceCode.getTokenBefore(operatorToken);
|
---|
| 265 | const firstRightToken = sourceCode.getTokenAfter(operatorToken);
|
---|
| 266 |
|
---|
| 267 | const source = sourceCode.getText();
|
---|
| 268 |
|
---|
| 269 | const leftText = source.slice(
|
---|
| 270 | node.range[0],
|
---|
| 271 | lastLeftToken.range[1]
|
---|
| 272 | );
|
---|
| 273 | const textBeforeOperator = source.slice(
|
---|
| 274 | lastLeftToken.range[1],
|
---|
| 275 | operatorToken.range[0]
|
---|
| 276 | );
|
---|
| 277 | const textAfterOperator = source.slice(
|
---|
| 278 | operatorToken.range[1],
|
---|
| 279 | firstRightToken.range[0]
|
---|
| 280 | );
|
---|
| 281 | const rightText = source.slice(
|
---|
| 282 | firstRightToken.range[0],
|
---|
| 283 | node.range[1]
|
---|
| 284 | );
|
---|
| 285 |
|
---|
| 286 | const tokenBefore = sourceCode.getTokenBefore(node);
|
---|
| 287 | const tokenAfter = sourceCode.getTokenAfter(node);
|
---|
| 288 | let prefix = "";
|
---|
| 289 | let suffix = "";
|
---|
| 290 |
|
---|
| 291 | if (
|
---|
| 292 | tokenBefore &&
|
---|
| 293 | tokenBefore.range[1] === node.range[0] &&
|
---|
| 294 | !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)
|
---|
| 295 | ) {
|
---|
| 296 | prefix = " ";
|
---|
| 297 | }
|
---|
| 298 |
|
---|
| 299 | if (
|
---|
| 300 | tokenAfter &&
|
---|
| 301 | node.range[1] === tokenAfter.range[0] &&
|
---|
| 302 | !astUtils.canTokensBeAdjacent(lastLeftToken, tokenAfter)
|
---|
| 303 | ) {
|
---|
| 304 | suffix = " ";
|
---|
| 305 | }
|
---|
| 306 |
|
---|
| 307 | return (
|
---|
| 308 | prefix +
|
---|
| 309 | rightText +
|
---|
| 310 | textBeforeOperator +
|
---|
| 311 | OPERATOR_FLIP_MAP[operatorToken.value] +
|
---|
| 312 | textAfterOperator +
|
---|
| 313 | leftText +
|
---|
| 314 | suffix
|
---|
| 315 | );
|
---|
| 316 | }
|
---|
| 317 |
|
---|
| 318 | //--------------------------------------------------------------------------
|
---|
| 319 | // Public
|
---|
| 320 | //--------------------------------------------------------------------------
|
---|
| 321 |
|
---|
| 322 | return {
|
---|
| 323 | BinaryExpression(node) {
|
---|
| 324 | const expectedLiteral = always ? node.left : node.right;
|
---|
| 325 | const expectedNonLiteral = always ? node.right : node.left;
|
---|
| 326 |
|
---|
| 327 | // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
|
---|
| 328 | if (
|
---|
| 329 | (expectedNonLiteral.type === "Literal" ||
|
---|
| 330 | looksLikeLiteral(expectedNonLiteral)) &&
|
---|
| 331 | !(
|
---|
| 332 | expectedLiteral.type === "Literal" ||
|
---|
| 333 | looksLikeLiteral(expectedLiteral)
|
---|
| 334 | ) &&
|
---|
| 335 | !(!isEqualityOperator(node.operator) && onlyEquality) &&
|
---|
| 336 | isComparisonOperator(node.operator) &&
|
---|
| 337 | !(exceptRange && isRangeTest(node.parent))
|
---|
| 338 | ) {
|
---|
| 339 | context.report({
|
---|
| 340 | node,
|
---|
| 341 | messageId: "expected",
|
---|
| 342 | data: {
|
---|
| 343 | operator: node.operator,
|
---|
| 344 | expectedSide: always ? "left" : "right"
|
---|
| 345 | },
|
---|
| 346 | fix: fixer =>
|
---|
| 347 | fixer.replaceText(node, getFlippedString(node))
|
---|
| 348 | });
|
---|
| 349 | }
|
---|
| 350 | }
|
---|
| 351 | };
|
---|
| 352 | }
|
---|
| 353 | };
|
---|