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 | 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 | };
|