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("./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
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};