1/** 2 * @fileoverview Rule to disallow mixed binary operators. 3 * @author Toru Nagashima 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils.js"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18const ARITHMETIC_OPERATORS = ["+", "-", "*", "/", "%", "**"]; 19const BITWISE_OPERATORS = ["&", "|", "^", "~", "<<", ">>", ">>>"]; 20const COMPARISON_OPERATORS = ["==", "!=", "===", "!==", ">", ">=", "<", "<="]; 21const LOGICAL_OPERATORS = ["&&", "||"]; 22const RELATIONAL_OPERATORS = ["in", "instanceof"]; 23const TERNARY_OPERATOR = ["?:"]; 24const COALESCE_OPERATOR = ["??"]; 25const ALL_OPERATORS = [].concat( 26 ARITHMETIC_OPERATORS, 27 BITWISE_OPERATORS, 28 COMPARISON_OPERATORS, 29 LOGICAL_OPERATORS, 30 RELATIONAL_OPERATORS, 31 TERNARY_OPERATOR, 32 COALESCE_OPERATOR 33); 34const DEFAULT_GROUPS = [ 35 ARITHMETIC_OPERATORS, 36 BITWISE_OPERATORS, 37 COMPARISON_OPERATORS, 38 LOGICAL_OPERATORS, 39 RELATIONAL_OPERATORS 40]; 41const TARGET_NODE_TYPE = /^(?:Binary|Logical|Conditional)Expression$/u; 42 43/** 44 * Normalizes options. 45 * @param {Object|undefined} options A options object to normalize. 46 * @returns {Object} Normalized option object. 47 */ 48function normalizeOptions(options = {}) { 49 const hasGroups = options.groups && options.groups.length > 0; 50 const groups = hasGroups ? options.groups : DEFAULT_GROUPS; 51 const allowSamePrecedence = options.allowSamePrecedence !== false; 52 53 return { 54 groups, 55 allowSamePrecedence 56 }; 57} 58 59/** 60 * Checks whether any group which includes both given operator exists or not. 61 * @param {Array.<string[]>} groups A list of groups to check. 62 * @param {string} left An operator. 63 * @param {string} right Another operator. 64 * @returns {boolean} `true` if such group existed. 65 */ 66function includesBothInAGroup(groups, left, right) { 67 return groups.some(group => group.indexOf(left) !== -1 && group.indexOf(right) !== -1); 68} 69 70/** 71 * Checks whether the given node is a conditional expression and returns the test node else the left node. 72 * @param {ASTNode} node A node which can be a BinaryExpression or a LogicalExpression node. 73 * This parent node can be BinaryExpression, LogicalExpression 74 * , or a ConditionalExpression node 75 * @returns {ASTNode} node the appropriate node(left or test). 76 */ 77function getChildNode(node) { 78 return node.type === "ConditionalExpression" ? node.test : node.left; 79} 80 81//------------------------------------------------------------------------------ 82// Rule Definition 83//------------------------------------------------------------------------------ 84 85module.exports = { 86 meta: { 87 type: "suggestion", 88 89 docs: { 90 description: "disallow mixed binary operators", 91 category: "Stylistic Issues", 92 recommended: false, 93 url: "https://eslint.org/docs/rules/no-mixed-operators" 94 }, 95 96 schema: [ 97 { 98 type: "object", 99 properties: { 100 groups: { 101 type: "array", 102 items: { 103 type: "array", 104 items: { enum: ALL_OPERATORS }, 105 minItems: 2, 106 uniqueItems: true 107 }, 108 uniqueItems: true 109 }, 110 allowSamePrecedence: { 111 type: "boolean", 112 default: true 113 } 114 }, 115 additionalProperties: false 116 } 117 ], 118 119 messages: { 120 unexpectedMixedOperator: "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'." 121 } 122 }, 123 124 create(context) { 125 const sourceCode = context.getSourceCode(); 126 const options = normalizeOptions(context.options[0]); 127 128 /** 129 * Checks whether a given node should be ignored by options or not. 130 * @param {ASTNode} node A node to check. This is a BinaryExpression 131 * node or a LogicalExpression node. This parent node is one of 132 * them, too. 133 * @returns {boolean} `true` if the node should be ignored. 134 */ 135 function shouldIgnore(node) { 136 const a = node; 137 const b = node.parent; 138 139 return ( 140 !includesBothInAGroup(options.groups, a.operator, b.type === "ConditionalExpression" ? "?:" : b.operator) || 141 ( 142 options.allowSamePrecedence && 143 astUtils.getPrecedence(a) === astUtils.getPrecedence(b) 144 ) 145 ); 146 } 147 148 /** 149 * Checks whether the operator of a given node is mixed with parent 150 * node's operator or not. 151 * @param {ASTNode} node A node to check. This is a BinaryExpression 152 * node or a LogicalExpression node. This parent node is one of 153 * them, too. 154 * @returns {boolean} `true` if the node was mixed. 155 */ 156 function isMixedWithParent(node) { 157 158 return ( 159 node.operator !== node.parent.operator && 160 !astUtils.isParenthesised(sourceCode, node) 161 ); 162 } 163 164 /** 165 * Checks whether the operator of a given node is mixed with a 166 * conditional expression. 167 * @param {ASTNode} node A node to check. This is a conditional 168 * expression node 169 * @returns {boolean} `true` if the node was mixed. 170 */ 171 function isMixedWithConditionalParent(node) { 172 return !astUtils.isParenthesised(sourceCode, node) && !astUtils.isParenthesised(sourceCode, node.test); 173 } 174 175 /** 176 * Gets the operator token of a given node. 177 * @param {ASTNode} node A node to check. This is a BinaryExpression 178 * node or a LogicalExpression node. 179 * @returns {Token} The operator token of the node. 180 */ 181 function getOperatorToken(node) { 182 return sourceCode.getTokenAfter(getChildNode(node), astUtils.isNotClosingParenToken); 183 } 184 185 /** 186 * Reports both the operator of a given node and the operator of the 187 * parent node. 188 * @param {ASTNode} node A node to check. This is a BinaryExpression 189 * node or a LogicalExpression node. This parent node is one of 190 * them, too. 191 * @returns {void} 192 */ 193 function reportBothOperators(node) { 194 const parent = node.parent; 195 const left = (getChildNode(parent) === node) ? node : parent; 196 const right = (getChildNode(parent) !== node) ? node : parent; 197 const data = { 198 leftOperator: left.operator || "?:", 199 rightOperator: right.operator || "?:" 200 }; 201 202 context.report({ 203 node: left, 204 loc: getOperatorToken(left).loc, 205 messageId: "unexpectedMixedOperator", 206 data 207 }); 208 context.report({ 209 node: right, 210 loc: getOperatorToken(right).loc, 211 messageId: "unexpectedMixedOperator", 212 data 213 }); 214 } 215 216 /** 217 * Checks between the operator of this node and the operator of the 218 * parent node. 219 * @param {ASTNode} node A node to check. 220 * @returns {void} 221 */ 222 function check(node) { 223 if (TARGET_NODE_TYPE.test(node.parent.type)) { 224 if (node.parent.type === "ConditionalExpression" && !shouldIgnore(node) && isMixedWithConditionalParent(node.parent)) { 225 reportBothOperators(node); 226 } else { 227 if (TARGET_NODE_TYPE.test(node.parent.type) && 228 isMixedWithParent(node) && 229 !shouldIgnore(node) 230 ) { 231 reportBothOperators(node); 232 } 233 } 234 } 235 236 } 237 238 return { 239 BinaryExpression: check, 240 LogicalExpression: check 241 }; 242 } 243}; 244