yoda.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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 ["<", "<="].includes(operator);
  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 isNegativeNumericLiteral(node) {
  46. return (
  47. node.type === "UnaryExpression" &&
  48. node.operator === "-" &&
  49. node.prefix &&
  50. astUtils.isNumericLiteral(node.argument)
  51. );
  52. }
  53. /**
  54. * Determines whether a node is a Template Literal which can be determined statically.
  55. * @param {ASTNode} node Node to test
  56. * @returns {boolean} True if the node is a Template Literal without expression.
  57. */
  58. function isStaticTemplateLiteral(node) {
  59. return node.type === "TemplateLiteral" && node.expressions.length === 0;
  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) || isStaticTemplateLiteral(node);
  68. }
  69. /**
  70. * Attempts to derive a Literal node from nodes that are treated like literals.
  71. * @param {ASTNode} node Node to normalize.
  72. * @returns {ASTNode} One of the following options.
  73. * 1. The original node if the node is already a Literal
  74. * 2. A normalized Literal node with the negative number as the value if the
  75. * node represents a negative number literal.
  76. * 3. A normalized Literal node with the string as the value if the node is
  77. * a Template Literal without expression.
  78. * 4. Otherwise `null`.
  79. */
  80. function getNormalizedLiteral(node) {
  81. if (node.type === "Literal") {
  82. return node;
  83. }
  84. if (isNegativeNumericLiteral(node)) {
  85. return {
  86. type: "Literal",
  87. value: -node.argument.value,
  88. raw: `-${node.argument.value}`
  89. };
  90. }
  91. if (isStaticTemplateLiteral(node)) {
  92. return {
  93. type: "Literal",
  94. value: node.quasis[0].value.cooked,
  95. raw: node.quasis[0].value.raw
  96. };
  97. }
  98. return null;
  99. }
  100. //------------------------------------------------------------------------------
  101. // Rule Definition
  102. //------------------------------------------------------------------------------
  103. /** @type {import('../shared/types').Rule} */
  104. module.exports = {
  105. meta: {
  106. type: "suggestion",
  107. docs: {
  108. description: 'Require or disallow "Yoda" conditions',
  109. recommended: false,
  110. url: "https://eslint.org/docs/rules/yoda"
  111. },
  112. schema: [
  113. {
  114. enum: ["always", "never"]
  115. },
  116. {
  117. type: "object",
  118. properties: {
  119. exceptRange: {
  120. type: "boolean",
  121. default: false
  122. },
  123. onlyEquality: {
  124. type: "boolean",
  125. default: false
  126. }
  127. },
  128. additionalProperties: false
  129. }
  130. ],
  131. fixable: "code",
  132. messages: {
  133. expected:
  134. "Expected literal to be on the {{expectedSide}} side of {{operator}}."
  135. }
  136. },
  137. create(context) {
  138. // Default to "never" (!always) if no option
  139. const always = context.options[0] === "always";
  140. const exceptRange =
  141. context.options[1] && context.options[1].exceptRange;
  142. const onlyEquality =
  143. context.options[1] && context.options[1].onlyEquality;
  144. const sourceCode = context.getSourceCode();
  145. /**
  146. * Determines whether node represents a range test.
  147. * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
  148. * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
  149. * both operators must be `<` or `<=`. Finally, the literal on the left side
  150. * must be less than or equal to the literal on the right side so that the
  151. * test makes any sense.
  152. * @param {ASTNode} node LogicalExpression node to test.
  153. * @returns {boolean} Whether node is a range test.
  154. */
  155. function isRangeTest(node) {
  156. const left = node.left,
  157. right = node.right;
  158. /**
  159. * Determines whether node is of the form `0 <= x && x < 1`.
  160. * @returns {boolean} Whether node is a "between" range test.
  161. */
  162. function isBetweenTest() {
  163. if (node.operator === "&&" && astUtils.isSameReference(left.right, right.left)) {
  164. const leftLiteral = getNormalizedLiteral(left.left);
  165. const rightLiteral = getNormalizedLiteral(right.right);
  166. if (leftLiteral === null && rightLiteral === null) {
  167. return false;
  168. }
  169. if (rightLiteral === null || leftLiteral === null) {
  170. return true;
  171. }
  172. if (leftLiteral.value <= rightLiteral.value) {
  173. return true;
  174. }
  175. }
  176. return false;
  177. }
  178. /**
  179. * Determines whether node is of the form `x < 0 || 1 <= x`.
  180. * @returns {boolean} Whether node is an "outside" range test.
  181. */
  182. function isOutsideTest() {
  183. if (node.operator === "||" && astUtils.isSameReference(left.left, right.right)) {
  184. const leftLiteral = getNormalizedLiteral(left.right);
  185. const rightLiteral = getNormalizedLiteral(right.left);
  186. if (leftLiteral === null && rightLiteral === null) {
  187. return false;
  188. }
  189. if (rightLiteral === null || leftLiteral === null) {
  190. return true;
  191. }
  192. if (leftLiteral.value <= rightLiteral.value) {
  193. return true;
  194. }
  195. }
  196. return false;
  197. }
  198. /**
  199. * Determines whether node is wrapped in parentheses.
  200. * @returns {boolean} Whether node is preceded immediately by an open
  201. * paren token and followed immediately by a close
  202. * paren token.
  203. */
  204. function isParenWrapped() {
  205. return astUtils.isParenthesised(sourceCode, node);
  206. }
  207. return (
  208. node.type === "LogicalExpression" &&
  209. left.type === "BinaryExpression" &&
  210. right.type === "BinaryExpression" &&
  211. isRangeTestOperator(left.operator) &&
  212. isRangeTestOperator(right.operator) &&
  213. (isBetweenTest() || isOutsideTest()) &&
  214. isParenWrapped()
  215. );
  216. }
  217. const OPERATOR_FLIP_MAP = {
  218. "===": "===",
  219. "!==": "!==",
  220. "==": "==",
  221. "!=": "!=",
  222. "<": ">",
  223. ">": "<",
  224. "<=": ">=",
  225. ">=": "<="
  226. };
  227. /**
  228. * Returns a string representation of a BinaryExpression node with its sides/operator flipped around.
  229. * @param {ASTNode} node The BinaryExpression node
  230. * @returns {string} A string representation of the node with the sides and operator flipped
  231. */
  232. function getFlippedString(node) {
  233. const operatorToken = sourceCode.getFirstTokenBetween(
  234. node.left,
  235. node.right,
  236. token => token.value === node.operator
  237. );
  238. const lastLeftToken = sourceCode.getTokenBefore(operatorToken);
  239. const firstRightToken = sourceCode.getTokenAfter(operatorToken);
  240. const source = sourceCode.getText();
  241. const leftText = source.slice(
  242. node.range[0],
  243. lastLeftToken.range[1]
  244. );
  245. const textBeforeOperator = source.slice(
  246. lastLeftToken.range[1],
  247. operatorToken.range[0]
  248. );
  249. const textAfterOperator = source.slice(
  250. operatorToken.range[1],
  251. firstRightToken.range[0]
  252. );
  253. const rightText = source.slice(
  254. firstRightToken.range[0],
  255. node.range[1]
  256. );
  257. const tokenBefore = sourceCode.getTokenBefore(node);
  258. const tokenAfter = sourceCode.getTokenAfter(node);
  259. let prefix = "";
  260. let suffix = "";
  261. if (
  262. tokenBefore &&
  263. tokenBefore.range[1] === node.range[0] &&
  264. !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)
  265. ) {
  266. prefix = " ";
  267. }
  268. if (
  269. tokenAfter &&
  270. node.range[1] === tokenAfter.range[0] &&
  271. !astUtils.canTokensBeAdjacent(lastLeftToken, tokenAfter)
  272. ) {
  273. suffix = " ";
  274. }
  275. return (
  276. prefix +
  277. rightText +
  278. textBeforeOperator +
  279. OPERATOR_FLIP_MAP[operatorToken.value] +
  280. textAfterOperator +
  281. leftText +
  282. suffix
  283. );
  284. }
  285. //--------------------------------------------------------------------------
  286. // Public
  287. //--------------------------------------------------------------------------
  288. return {
  289. BinaryExpression(node) {
  290. const expectedLiteral = always ? node.left : node.right;
  291. const expectedNonLiteral = always ? node.right : node.left;
  292. // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
  293. if (
  294. (expectedNonLiteral.type === "Literal" ||
  295. looksLikeLiteral(expectedNonLiteral)) &&
  296. !(
  297. expectedLiteral.type === "Literal" ||
  298. looksLikeLiteral(expectedLiteral)
  299. ) &&
  300. !(!isEqualityOperator(node.operator) && onlyEquality) &&
  301. isComparisonOperator(node.operator) &&
  302. !(exceptRange && isRangeTest(context.getAncestors().pop()))
  303. ) {
  304. context.report({
  305. node,
  306. messageId: "expected",
  307. data: {
  308. operator: node.operator,
  309. expectedSide: always ? "left" : "right"
  310. },
  311. fix: fixer =>
  312. fixer.replaceText(node, getFlippedString(node))
  313. });
  314. }
  315. }
  316. };
  317. }
  318. };