1 | /**
|
2 | * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before
|
3 | * @author Benoît Zugmeyer
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const astUtils = require("../ast-utils");
|
13 |
|
14 | //------------------------------------------------------------------------------
|
15 | // Rule Definition
|
16 | //------------------------------------------------------------------------------
|
17 |
|
18 | module.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 | };
|