yoda.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. /**
  2. * @fileoverview Rule to require or disallow yoda comparisons
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //--------------------------------------------------------------------------
  7. // Requirements
  8. //--------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //--------------------------------------------------------------------------
  11. // Helpers
  12. //--------------------------------------------------------------------------
  13. /**
  14. * Determines whether an operator is a comparison operator.
  15. * @param {string} operator The operator to check.
  16. * @returns {boolean} Whether or not it is a comparison operator.
  17. */
  18. function isComparisonOperator(operator) {
  19. return (/^(==|===|!=|!==|<|>|<=|>=)$/u).test(operator);
  20. }
  21. /**
  22. * Determines whether an operator is an equality operator.
  23. * @param {string} operator The operator to check.
  24. * @returns {boolean} Whether or not it is an equality operator.
  25. */
  26. function isEqualityOperator(operator) {
  27. return (/^(==|===)$/u).test(operator);
  28. }
  29. /**
  30. * Determines whether an operator is one used in a range test.
  31. * Allowed operators are `<` and `<=`.
  32. * @param {string} operator The operator to check.
  33. * @returns {boolean} Whether the operator is used in range tests.
  34. */
  35. function isRangeTestOperator(operator) {
  36. return ["<", "<="].indexOf(operator) >= 0;
  37. }
  38. /**
  39. * Determines whether a non-Literal node is a negative number that should be
  40. * treated as if it were a single Literal node.
  41. * @param {ASTNode} node Node to test.
  42. * @returns {boolean} True if the node is a negative number that looks like a
  43. * real literal and should be treated as such.
  44. */
  45. function looksLikeLiteral(node) {
  46. return (node.type === "UnaryExpression" &&
  47. node.operator === "-" &&
  48. node.prefix &&
  49. node.argument.type === "Literal" &&
  50. typeof node.argument.value === "number");
  51. }
  52. /**
  53. * Attempts to derive a Literal node from nodes that are treated like literals.
  54. * @param {ASTNode} node Node to normalize.
  55. * @param {number} [defaultValue] The default value to be returned if the node
  56. * is not a Literal.
  57. * @returns {ASTNode} One of the following options.
  58. * 1. The original node if the node is already a Literal
  59. * 2. A normalized Literal node with the negative number as the value if the
  60. * node represents a negative number literal.
  61. * 3. The Literal node which has the `defaultValue` argument if it exists.
  62. * 4. Otherwise `null`.
  63. */
  64. function getNormalizedLiteral(node, defaultValue) {
  65. if (node.type === "Literal") {
  66. return node;
  67. }
  68. if (looksLikeLiteral(node)) {
  69. return {
  70. type: "Literal",
  71. value: -node.argument.value,
  72. raw: `-${node.argument.value}`
  73. };
  74. }
  75. if (defaultValue) {
  76. return {
  77. type: "Literal",
  78. value: defaultValue,
  79. raw: String(defaultValue)
  80. };
  81. }
  82. return null;
  83. }
  84. /**
  85. * Checks whether two expressions reference the same value. For example:
  86. * a = a
  87. * a.b = a.b
  88. * a[0] = a[0]
  89. * a['b'] = a['b']
  90. * @param {ASTNode} a Left side of the comparison.
  91. * @param {ASTNode} b Right side of the comparison.
  92. * @returns {boolean} True if both sides match and reference the same value.
  93. */
  94. function same(a, b) {
  95. if (a.type !== b.type) {
  96. return false;
  97. }
  98. switch (a.type) {
  99. case "Identifier":
  100. return a.name === b.name;
  101. case "Literal":
  102. return a.value === b.value;
  103. case "MemberExpression": {
  104. const nameA = astUtils.getStaticPropertyName(a);
  105. // x.y = x["y"]
  106. if (nameA !== null) {
  107. return (
  108. same(a.object, b.object) &&
  109. nameA === astUtils.getStaticPropertyName(b)
  110. );
  111. }
  112. /*
  113. * x[0] = x[0]
  114. * x[y] = x[y]
  115. * x.y = x.y
  116. */
  117. return (
  118. a.computed === b.computed &&
  119. same(a.object, b.object) &&
  120. same(a.property, b.property)
  121. );
  122. }
  123. case "ThisExpression":
  124. return true;
  125. default:
  126. return false;
  127. }
  128. }
  129. //------------------------------------------------------------------------------
  130. // Rule Definition
  131. //------------------------------------------------------------------------------
  132. module.exports = {
  133. meta: {
  134. type: "suggestion",
  135. docs: {
  136. description: "require or disallow \"Yoda\" conditions",
  137. category: "Best Practices",
  138. recommended: false,
  139. url: "https://eslint.org/docs/rules/yoda"
  140. },
  141. schema: [
  142. {
  143. enum: ["always", "never"]
  144. },
  145. {
  146. type: "object",
  147. properties: {
  148. exceptRange: {
  149. type: "boolean",
  150. default: false
  151. },
  152. onlyEquality: {
  153. type: "boolean",
  154. default: false
  155. }
  156. },
  157. additionalProperties: false
  158. }
  159. ],
  160. fixable: "code",
  161. messages: {
  162. expected: "Expected literal to be on the {{expectedSide}} side of {{operator}}."
  163. }
  164. },
  165. create(context) {
  166. // Default to "never" (!always) if no option
  167. const always = (context.options[0] === "always");
  168. const exceptRange = (context.options[1] && context.options[1].exceptRange);
  169. const onlyEquality = (context.options[1] && context.options[1].onlyEquality);
  170. const sourceCode = context.getSourceCode();
  171. /**
  172. * Determines whether node represents a range test.
  173. * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
  174. * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
  175. * both operators must be `<` or `<=`. Finally, the literal on the left side
  176. * must be less than or equal to the literal on the right side so that the
  177. * test makes any sense.
  178. * @param {ASTNode} node LogicalExpression node to test.
  179. * @returns {boolean} Whether node is a range test.
  180. */
  181. function isRangeTest(node) {
  182. const left = node.left,
  183. right = node.right;
  184. /**
  185. * Determines whether node is of the form `0 <= x && x < 1`.
  186. * @returns {boolean} Whether node is a "between" range test.
  187. */
  188. function isBetweenTest() {
  189. let leftLiteral, rightLiteral;
  190. return (node.operator === "&&" &&
  191. (leftLiteral = getNormalizedLiteral(left.left)) &&
  192. (rightLiteral = getNormalizedLiteral(right.right, Number.POSITIVE_INFINITY)) &&
  193. leftLiteral.value <= rightLiteral.value &&
  194. same(left.right, right.left));
  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. let leftLiteral, rightLiteral;
  202. return (node.operator === "||" &&
  203. (leftLiteral = getNormalizedLiteral(left.right, Number.NEGATIVE_INFINITY)) &&
  204. (rightLiteral = getNormalizedLiteral(right.left)) &&
  205. leftLiteral.value <= rightLiteral.value &&
  206. same(left.left, right.right));
  207. }
  208. /**
  209. * Determines whether node is wrapped in parentheses.
  210. * @returns {boolean} Whether node is preceded immediately by an open
  211. * paren token and followed immediately by a close
  212. * paren token.
  213. */
  214. function isParenWrapped() {
  215. return astUtils.isParenthesised(sourceCode, node);
  216. }
  217. return (node.type === "LogicalExpression" &&
  218. left.type === "BinaryExpression" &&
  219. right.type === "BinaryExpression" &&
  220. isRangeTestOperator(left.operator) &&
  221. isRangeTestOperator(right.operator) &&
  222. (isBetweenTest() || isOutsideTest()) &&
  223. isParenWrapped());
  224. }
  225. const OPERATOR_FLIP_MAP = {
  226. "===": "===",
  227. "!==": "!==",
  228. "==": "==",
  229. "!=": "!=",
  230. "<": ">",
  231. ">": "<",
  232. "<=": ">=",
  233. ">=": "<="
  234. };
  235. /**
  236. * Returns a string representation of a BinaryExpression node with its sides/operator flipped around.
  237. * @param {ASTNode} node The BinaryExpression node
  238. * @returns {string} A string representation of the node with the sides and operator flipped
  239. */
  240. function getFlippedString(node) {
  241. const tokenBefore = sourceCode.getTokenBefore(node);
  242. const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
  243. const textBeforeOperator = sourceCode.getText().slice(sourceCode.getTokenBefore(operatorToken).range[1], operatorToken.range[0]);
  244. const textAfterOperator = sourceCode.getText().slice(operatorToken.range[1], sourceCode.getTokenAfter(operatorToken).range[0]);
  245. const leftText = sourceCode.getText().slice(node.range[0], sourceCode.getTokenBefore(operatorToken).range[1]);
  246. const firstRightToken = sourceCode.getTokenAfter(operatorToken);
  247. const rightText = sourceCode.getText().slice(firstRightToken.range[0], node.range[1]);
  248. let prefix = "";
  249. if (tokenBefore && tokenBefore.range[1] === node.range[0] &&
  250. !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)) {
  251. prefix = " ";
  252. }
  253. return prefix + rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText;
  254. }
  255. //--------------------------------------------------------------------------
  256. // Public
  257. //--------------------------------------------------------------------------
  258. return {
  259. BinaryExpression(node) {
  260. const expectedLiteral = always ? node.left : node.right;
  261. const expectedNonLiteral = always ? node.right : node.left;
  262. // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
  263. if (
  264. (expectedNonLiteral.type === "Literal" || looksLikeLiteral(expectedNonLiteral)) &&
  265. !(expectedLiteral.type === "Literal" || looksLikeLiteral(expectedLiteral)) &&
  266. !(!isEqualityOperator(node.operator) && onlyEquality) &&
  267. isComparisonOperator(node.operator) &&
  268. !(exceptRange && isRangeTest(context.getAncestors().pop()))
  269. ) {
  270. context.report({
  271. node,
  272. messageId: "expected",
  273. data: {
  274. operator: node.operator,
  275. expectedSide: always ? "left" : "right"
  276. },
  277. fix: fixer => fixer.replaceText(node, getFlippedString(node))
  278. });
  279. }
  280. }
  281. };
  282. }
  283. };