1/** 2 * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before 3 * @author Benoît Zugmeyer 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Rule Definition 16//------------------------------------------------------------------------------ 17 18module.exports = { 19 meta: { 20 type: "layout", 21 22 docs: { 23 description: "enforce consistent linebreak style for operators", 24 category: "Stylistic Issues", 25 recommended: false, 26 url: "https://eslint.org/docs/rules/operator-linebreak" 27 }, 28 29 schema: [ 30 { 31 enum: ["after", "before", "none", null] 32 }, 33 { 34 type: "object", 35 properties: { 36 overrides: { 37 type: "object", 38 properties: { 39 anyOf: { 40 type: "string", 41 enum: ["after", "before", "none", "ignore"] 42 } 43 } 44 } 45 }, 46 additionalProperties: false 47 } 48 ], 49 50 fixable: "code", 51 52 messages: { 53 operatorAtBeginning: "'{{operator}}' should be placed at the beginning of the line.", 54 operatorAtEnd: "'{{operator}}' should be placed at the end of the line.", 55 badLinebreak: "Bad line breaking before and after '{{operator}}'.", 56 noLinebreak: "There should be no line break before or after '{{operator}}'." 57 } 58 }, 59 60 create(context) { 61 62 const usedDefaultGlobal = !context.options[0]; 63 const globalStyle = context.options[0] || "after"; 64 const options = context.options[1] || {}; 65 const styleOverrides = options.overrides ? Object.assign({}, options.overrides) : {}; 66 67 if (usedDefaultGlobal && !styleOverrides["?"]) { 68 styleOverrides["?"] = "before"; 69 } 70 71 if (usedDefaultGlobal && !styleOverrides[":"]) { 72 styleOverrides[":"] = "before"; 73 } 74 75 const sourceCode = context.getSourceCode(); 76 77 //-------------------------------------------------------------------------- 78 // Helpers 79 //-------------------------------------------------------------------------- 80 81 /** 82 * Gets a fixer function to fix rule issues 83 * @param {Token} operatorToken The operator token of an expression 84 * @param {string} desiredStyle The style for the rule. One of 'before', 'after', 'none' 85 * @returns {Function} A fixer function 86 */ 87 function getFixer(operatorToken, desiredStyle) { 88 return fixer => { 89 const tokenBefore = sourceCode.getTokenBefore(operatorToken); 90 const tokenAfter = sourceCode.getTokenAfter(operatorToken); 91 const textBefore = sourceCode.text.slice(tokenBefore.range[1], operatorToken.range[0]); 92 const textAfter = sourceCode.text.slice(operatorToken.range[1], tokenAfter.range[0]); 93 const hasLinebreakBefore = !astUtils.isTokenOnSameLine(tokenBefore, operatorToken); 94 const hasLinebreakAfter = !astUtils.isTokenOnSameLine(operatorToken, tokenAfter); 95 let newTextBefore, newTextAfter; 96 97 if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") { 98 99 // If there is a comment before and after the operator, don't do a fix. 100 if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore && 101 sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) { 102 103 return null; 104 } 105 106 /* 107 * If there is only one linebreak and it's on the wrong side of the operator, swap the text before and after the operator. 108 * foo && 109 * bar 110 * would get fixed to 111 * foo 112 * && bar 113 */ 114 newTextBefore = textAfter; 115 newTextAfter = textBefore; 116 } else { 117 const LINEBREAK_REGEX = astUtils.createGlobalLinebreakMatcher(); 118 119 // Otherwise, if no linebreak is desired and no comments interfere, replace the linebreaks with empty strings. 120 newTextBefore = desiredStyle === "before" || textBefore.trim() ? textBefore : textBefore.replace(LINEBREAK_REGEX, ""); 121 newTextAfter = desiredStyle === "after" || textAfter.trim() ? textAfter : textAfter.replace(LINEBREAK_REGEX, ""); 122 123 // If there was no change (due to interfering comments), don't output a fix. 124 if (newTextBefore === textBefore && newTextAfter === textAfter) { 125 return null; 126 } 127 } 128 129 if (newTextAfter === "" && tokenAfter.type === "Punctuator" && "+-".includes(operatorToken.value) && tokenAfter.value === operatorToken.value) { 130 131 // To avoid accidentally creating a ++ or -- operator, insert a space if the operator is a +/- and the following token is a unary +/-. 132 newTextAfter += " "; 133 } 134 135 return fixer.replaceTextRange([tokenBefore.range[1], tokenAfter.range[0]], newTextBefore + operatorToken.value + newTextAfter); 136 }; 137 } 138 139 /** 140 * Checks the operator placement 141 * @param {ASTNode} node The node to check 142 * @param {ASTNode} leftSide The node that comes before the operator in `node` 143 * @private 144 * @returns {void} 145 */ 146 function validateNode(node, leftSide) { 147 148 /* 149 * When the left part of a binary expression is a single expression wrapped in 150 * parentheses (ex: `(a) + b`), leftToken will be the last token of the expression 151 * and operatorToken will be the closing parenthesis. 152 * The leftToken should be the last closing parenthesis, and the operatorToken 153 * should be the token right after that. 154 */ 155 const operatorToken = sourceCode.getTokenAfter(leftSide, astUtils.isNotClosingParenToken); 156 const leftToken = sourceCode.getTokenBefore(operatorToken); 157 const rightToken = sourceCode.getTokenAfter(operatorToken); 158 const operator = operatorToken.value; 159 const operatorStyleOverride = styleOverrides[operator]; 160 const style = operatorStyleOverride || globalStyle; 161 const fix = getFixer(operatorToken, style); 162 163 // if single line 164 if (astUtils.isTokenOnSameLine(leftToken, operatorToken) && 165 astUtils.isTokenOnSameLine(operatorToken, rightToken)) { 166 167 // do nothing. 168 169 } else if (operatorStyleOverride !== "ignore" && !astUtils.isTokenOnSameLine(leftToken, operatorToken) && 170 !astUtils.isTokenOnSameLine(operatorToken, rightToken)) { 171 172 // lone operator 173 context.report({ 174 node, 175 loc: operatorToken.loc, 176 messageId: "badLinebreak", 177 data: { 178 operator 179 }, 180 fix 181 }); 182 183 } else if (style === "before" && astUtils.isTokenOnSameLine(leftToken, operatorToken)) { 184 185 context.report({ 186 node, 187 loc: operatorToken.loc, 188 messageId: "operatorAtBeginning", 189 data: { 190 operator 191 }, 192 fix 193 }); 194 195 } else if (style === "after" && astUtils.isTokenOnSameLine(operatorToken, rightToken)) { 196 197 context.report({ 198 node, 199 loc: operatorToken.loc, 200 messageId: "operatorAtEnd", 201 data: { 202 operator 203 }, 204 fix 205 }); 206 207 } else if (style === "none") { 208 209 context.report({ 210 node, 211 loc: operatorToken.loc, 212 messageId: "noLinebreak", 213 data: { 214 operator 215 }, 216 fix 217 }); 218 219 } 220 } 221 222 /** 223 * Validates a binary expression using `validateNode` 224 * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated 225 * @returns {void} 226 */ 227 function validateBinaryExpression(node) { 228 validateNode(node, node.left); 229 } 230 231 //-------------------------------------------------------------------------- 232 // Public 233 //-------------------------------------------------------------------------- 234 235 return { 236 BinaryExpression: validateBinaryExpression, 237 LogicalExpression: validateBinaryExpression, 238 AssignmentExpression: validateBinaryExpression, 239 VariableDeclarator(node) { 240 if (node.init) { 241 validateNode(node, node.id); 242 } 243 }, 244 ConditionalExpression(node) { 245 validateNode(node, node.test); 246 validateNode(node, node.consequent); 247 } 248 }; 249 } 250}; 251