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