UNPKG

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