2 * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before
3 * @author BenoƮt Zugmeyer
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
23 description: "enforce consistent linebreak style for operators",
24 category: "Stylistic Issues",
26 url: "https://eslint.org/docs/rules/operator-linebreak"
31 enum: ["after", "before", "none", null]
41 enum: ["after", "before", "none", "ignore"]
46 additionalProperties: false
55 const usedDefaultGlobal = !context.options[0];
56 const globalStyle = context.options[0] || "after";
57 const options = context.options[1] || {};
58 const styleOverrides = options.overrides ? Object.assign({}, options.overrides) : {};
60 if (usedDefaultGlobal && !styleOverrides["?"]) {
61 styleOverrides["?"] = "before";
64 if (usedDefaultGlobal && !styleOverrides[":"]) {
65 styleOverrides[":"] = "before";
68 const sourceCode = context.getSourceCode();
70 //--------------------------------------------------------------------------
72 //--------------------------------------------------------------------------
75 * Gets a fixer function to fix rule issues
76 * @param {Token} operatorToken The operator token of an expression
77 * @param {string} desiredStyle The style for the rule. One of 'before', 'after', 'none'
78 * @returns {Function} A fixer function
80 function getFixer(operatorToken, desiredStyle) {
82 const tokenBefore = sourceCode.getTokenBefore(operatorToken);
83 const tokenAfter = sourceCode.getTokenAfter(operatorToken);
84 const textBefore = sourceCode.text.slice(tokenBefore.range[1], operatorToken.range[0]);
85 const textAfter = sourceCode.text.slice(operatorToken.range[1], tokenAfter.range[0]);
86 const hasLinebreakBefore = !astUtils.isTokenOnSameLine(tokenBefore, operatorToken);
87 const hasLinebreakAfter = !astUtils.isTokenOnSameLine(operatorToken, tokenAfter);
88 let newTextBefore, newTextAfter;
90 if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") {
92 // If there is a comment before and after the operator, don't do a fix.
93 if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore &&
94 sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) {
100 * If there is only one linebreak and it's on the wrong side of the operator, swap the text before and after the operator.
107 newTextBefore = textAfter;
108 newTextAfter = textBefore;
110 const LINEBREAK_REGEX = astUtils.createGlobalLinebreakMatcher();
112 // Otherwise, if no linebreak is desired and no comments interfere, replace the linebreaks with empty strings.
113 newTextBefore = desiredStyle === "before" || textBefore.trim() ? textBefore : textBefore.replace(LINEBREAK_REGEX, "");
114 newTextAfter = desiredStyle === "after" || textAfter.trim() ? textAfter : textAfter.replace(LINEBREAK_REGEX, "");
116 // If there was no change (due to interfering comments), don't output a fix.
117 if (newTextBefore === textBefore && newTextAfter === textAfter) {
122 if (newTextAfter === "" && tokenAfter.type === "Punctuator" && "+-".includes(operatorToken.value) && tokenAfter.value === operatorToken.value) {
124 // To avoid accidentally creating a ++ or -- operator, insert a space if the operator is a +/- and the following token is a unary +/-.
128 return fixer.replaceTextRange([tokenBefore.range[1], tokenAfter.range[0]], newTextBefore + operatorToken.value + newTextAfter);
133 * Checks the operator placement
134 * @param {ASTNode} node The node to check
135 * @param {ASTNode} leftSide The node that comes before the operator in `node`
139 function validateNode(node, leftSide) {
142 * When the left part of a binary expression is a single expression wrapped in
143 * parentheses (ex: `(a) + b`), leftToken will be the last token of the expression
144 * and operatorToken will be the closing parenthesis.
145 * The leftToken should be the last closing parenthesis, and the operatorToken
146 * should be the token right after that.
148 const operatorToken = sourceCode.getTokenAfter(leftSide, astUtils.isNotClosingParenToken);
149 const leftToken = sourceCode.getTokenBefore(operatorToken);
150 const rightToken = sourceCode.getTokenAfter(operatorToken);
151 const operator = operatorToken.value;
152 const operatorStyleOverride = styleOverrides[operator];
153 const style = operatorStyleOverride || globalStyle;
154 const fix = getFixer(operatorToken, style);
157 if (astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
158 astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
162 } else if (operatorStyleOverride !== "ignore" && !astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
163 !astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
169 line: operatorToken.loc.end.line,
170 column: operatorToken.loc.end.column
172 message: "Bad line breaking before and after '{{operator}}'.",
179 } else if (style === "before" && astUtils.isTokenOnSameLine(leftToken, operatorToken)) {
184 line: operatorToken.loc.end.line,
185 column: operatorToken.loc.end.column
187 message: "'{{operator}}' should be placed at the beginning of the line.",
194 } else if (style === "after" && astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
199 line: operatorToken.loc.end.line,
200 column: operatorToken.loc.end.column
202 message: "'{{operator}}' should be placed at the end of the line.",
209 } else if (style === "none") {
214 line: operatorToken.loc.end.line,
215 column: operatorToken.loc.end.column
217 message: "There should be no line break before or after '{{operator}}'.",
228 * Validates a binary expression using `validateNode`
229 * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated
232 function validateBinaryExpression(node) {
233 validateNode(node, node.left);
236 //--------------------------------------------------------------------------
238 //--------------------------------------------------------------------------
241 BinaryExpression: validateBinaryExpression,
242 LogicalExpression: validateBinaryExpression,
243 AssignmentExpression: validateBinaryExpression,
244 VariableDeclarator(node) {
246 validateNode(node, node.id);
249 ConditionalExpression(node) {
250 validateNode(node, node.test);
251 validateNode(node, node.consequent);