]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/operator-linebreak.js
3395feae655f64d4348ffcab8fe227e3c22c82c3
[pve-eslint.git] / eslint / lib / rules / 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 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 };