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