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