UNPKG

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