no-cond-assign.js 5.5 KB

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