]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | ||
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", | |
6f036462 TL |
38 | additionalProperties: { |
39 | enum: ["after", "before", "none", "ignore"] | |
eb39fafa DC |
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 | }; |