no-cond-assign.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /**
  2. * @fileoverview Rule to flag assignment in a conditional statement's test expression
  3. * @author Stephen Murray <spmurrayzzz>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const TEST_CONDITION_PARENT_TYPES = new Set([
  14. "IfStatement",
  15. "WhileStatement",
  16. "DoWhileStatement",
  17. "ForStatement",
  18. "ConditionalExpression",
  19. ]);
  20. const NODE_DESCRIPTIONS = {
  21. DoWhileStatement: "a 'do...while' statement",
  22. ForStatement: "a 'for' statement",
  23. IfStatement: "an 'if' statement",
  24. WhileStatement: "a 'while' statement",
  25. };
  26. //------------------------------------------------------------------------------
  27. // Rule Definition
  28. //------------------------------------------------------------------------------
  29. /** @type {import('../types').Rule.RuleModule} */
  30. module.exports = {
  31. meta: {
  32. type: "problem",
  33. defaultOptions: ["except-parens"],
  34. docs: {
  35. description:
  36. "Disallow assignment operators in conditional expressions",
  37. recommended: true,
  38. url: "https://eslint.org/docs/latest/rules/no-cond-assign",
  39. },
  40. schema: [
  41. {
  42. enum: ["except-parens", "always"],
  43. },
  44. ],
  45. messages: {
  46. unexpected: "Unexpected assignment within {{type}}.",
  47. // must match JSHint's error message
  48. missing:
  49. "Expected a conditional expression and instead saw an assignment.",
  50. },
  51. },
  52. create(context) {
  53. const [prohibitAssign] = context.options;
  54. const sourceCode = context.sourceCode;
  55. /**
  56. * Check whether an AST node is the test expression for a conditional statement.
  57. * @param {!Object} node The node to test.
  58. * @returns {boolean} `true` if the node is the text expression for a conditional statement; otherwise, `false`.
  59. */
  60. function isConditionalTestExpression(node) {
  61. return (
  62. node.parent &&
  63. TEST_CONDITION_PARENT_TYPES.has(node.parent.type) &&
  64. node === node.parent.test
  65. );
  66. }
  67. /**
  68. * Given an AST node, perform a bottom-up search for the first ancestor that represents a conditional statement.
  69. * @param {!Object} node The node to use at the start of the search.
  70. * @returns {?Object} The closest ancestor node that represents a conditional statement.
  71. */
  72. function findConditionalAncestor(node) {
  73. let currentAncestor = node;
  74. do {
  75. if (isConditionalTestExpression(currentAncestor)) {
  76. return currentAncestor.parent;
  77. }
  78. } while (
  79. (currentAncestor = currentAncestor.parent) &&
  80. !astUtils.isFunction(currentAncestor)
  81. );
  82. return null;
  83. }
  84. /**
  85. * Check whether the code represented by an AST node is enclosed in two sets of parentheses.
  86. * @param {!Object} node The node to test.
  87. * @returns {boolean} `true` if the code is enclosed in two sets of parentheses; otherwise, `false`.
  88. */
  89. function isParenthesisedTwice(node) {
  90. const previousToken = sourceCode.getTokenBefore(node, 1),
  91. nextToken = sourceCode.getTokenAfter(node, 1);
  92. return (
  93. astUtils.isParenthesised(sourceCode, node) &&
  94. previousToken &&
  95. astUtils.isOpeningParenToken(previousToken) &&
  96. previousToken.range[1] <= node.range[0] &&
  97. astUtils.isClosingParenToken(nextToken) &&
  98. nextToken.range[0] >= node.range[1]
  99. );
  100. }
  101. /**
  102. * Check a conditional statement's test expression for top-level assignments that are not enclosed in parentheses.
  103. * @param {!Object} node The node for the conditional statement.
  104. * @returns {void}
  105. */
  106. function testForAssign(node) {
  107. if (
  108. node.test &&
  109. node.test.type === "AssignmentExpression" &&
  110. (node.type === "ForStatement"
  111. ? !astUtils.isParenthesised(sourceCode, node.test)
  112. : !isParenthesisedTwice(node.test))
  113. ) {
  114. context.report({
  115. node: node.test,
  116. messageId: "missing",
  117. });
  118. }
  119. }
  120. /**
  121. * Check whether an assignment expression is descended from a conditional statement's test expression.
  122. * @param {!Object} node The node for the assignment expression.
  123. * @returns {void}
  124. */
  125. function testForConditionalAncestor(node) {
  126. const ancestor = findConditionalAncestor(node);
  127. if (ancestor) {
  128. context.report({
  129. node,
  130. messageId: "unexpected",
  131. data: {
  132. type: NODE_DESCRIPTIONS[ancestor.type] || ancestor.type,
  133. },
  134. });
  135. }
  136. }
  137. if (prohibitAssign === "always") {
  138. return {
  139. AssignmentExpression: testForConditionalAncestor,
  140. };
  141. }
  142. return {
  143. DoWhileStatement: testForAssign,
  144. ForStatement: testForAssign,
  145. IfStatement: testForAssign,
  146. WhileStatement: testForAssign,
  147. ConditionalExpression: testForAssign,
  148. };
  149. },
  150. };