bce0ef56a0cc6cbc2acff72f068acbf17c08d65f
[dotfiles/.git] / operator-linebreak.js
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
12 const astUtils = require("./utils/ast-utils");
13
14 //------------------------------------------------------------------------------
15 // Rule Definition
16 //------------------------------------------------------------------------------
17
18 module.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
53     create(context) {
54
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) : {};
59
60         if (usedDefaultGlobal && !styleOverrides["?"]) {
61             styleOverrides["?"] = "before";
62         }
63
64         if (usedDefaultGlobal && !styleOverrides[":"]) {
65             styleOverrides[":"] = "before";
66         }
67
68         const sourceCode = context.getSourceCode();
69
70         //--------------------------------------------------------------------------
71         // Helpers
72         //--------------------------------------------------------------------------
73
74         /**
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
79          */
80         function getFixer(operatorToken, desiredStyle) {
81             return fixer => {
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;
89
90                 if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") {
91
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) {
95
96                         return null;
97                     }
98
99                     /*
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.
101                      * foo &&
102                      *           bar
103                      * would get fixed to
104                      * foo
105                      *        && bar
106                      */
107                     newTextBefore = textAfter;
108                     newTextAfter = textBefore;
109                 } else {
110                     const LINEBREAK_REGEX = astUtils.createGlobalLinebreakMatcher();
111
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, "");
115
116                     // If there was no change (due to interfering comments), don't output a fix.
117                     if (newTextBefore === textBefore && newTextAfter === textAfter) {
118                         return null;
119                     }
120                 }
121
122                 if (newTextAfter === "" && tokenAfter.type === "Punctuator" && "+-".includes(operatorToken.value) && tokenAfter.value === operatorToken.value) {
123
124                     // To avoid accidentally creating a ++ or -- operator, insert a space if the operator is a +/- and the following token is a unary +/-.
125                     newTextAfter += " ";
126                 }
127
128                 return fixer.replaceTextRange([tokenBefore.range[1], tokenAfter.range[0]], newTextBefore + operatorToken.value + newTextAfter);
129             };
130         }
131
132         /**
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`
136          * @private
137          * @returns {void}
138          */
139         function validateNode(node, leftSide) {
140
141             /*
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.
147              */
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);
155
156             // if single line
157             if (astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
158                     astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
159
160                 // do nothing.
161
162             } else if (operatorStyleOverride !== "ignore" && !astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
163                     !astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
164
165                 // lone operator
166                 context.report({
167                     node,
168                     loc: {
169                         line: operatorToken.loc.end.line,
170                         column: operatorToken.loc.end.column
171                     },
172                     message: "Bad line breaking before and after '{{operator}}'.",
173                     data: {
174                         operator
175                     },
176                     fix
177                 });
178
179             } else if (style === "before" && astUtils.isTokenOnSameLine(leftToken, operatorToken)) {
180
181                 context.report({
182                     node,
183                     loc: {
184                         line: operatorToken.loc.end.line,
185                         column: operatorToken.loc.end.column
186                     },
187                     message: "'{{operator}}' should be placed at the beginning of the line.",
188                     data: {
189                         operator
190                     },
191                     fix
192                 });
193
194             } else if (style === "after" && astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
195
196                 context.report({
197                     node,
198                     loc: {
199                         line: operatorToken.loc.end.line,
200                         column: operatorToken.loc.end.column
201                     },
202                     message: "'{{operator}}' should be placed at the end of the line.",
203                     data: {
204                         operator
205                     },
206                     fix
207                 });
208
209             } else if (style === "none") {
210
211                 context.report({
212                     node,
213                     loc: {
214                         line: operatorToken.loc.end.line,
215                         column: operatorToken.loc.end.column
216                     },
217                     message: "There should be no line break before or after '{{operator}}'.",
218                     data: {
219                         operator
220                     },
221                     fix
222                 });
223
224             }
225         }
226
227         /**
228          * Validates a binary expression using `validateNode`
229          * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated
230          * @returns {void}
231          */
232         function validateBinaryExpression(node) {
233             validateNode(node, node.left);
234         }
235
236         //--------------------------------------------------------------------------
237         // Public
238         //--------------------------------------------------------------------------
239
240         return {
241             BinaryExpression: validateBinaryExpression,
242             LogicalExpression: validateBinaryExpression,
243             AssignmentExpression: validateBinaryExpression,
244             VariableDeclarator(node) {
245                 if (node.init) {
246                     validateNode(node, node.id);
247                 }
248             },
249             ConditionalExpression(node) {
250                 validateNode(node, node.test);
251                 validateNode(node, node.consequent);
252             }
253         };
254     }
255 };