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