• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview This rule shoud require or disallow spaces before or after unary operations.
3 * @author Marcin Kumorek
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const astUtils = require("./utils/ast-utils");
12
13//------------------------------------------------------------------------------
14// Rule Definition
15//------------------------------------------------------------------------------
16
17module.exports = {
18    meta: {
19        type: "layout",
20
21        docs: {
22            description: "enforce consistent spacing before or after unary operators",
23            category: "Stylistic Issues",
24            recommended: false,
25            url: "https://eslint.org/docs/rules/space-unary-ops"
26        },
27
28        fixable: "whitespace",
29
30        schema: [
31            {
32                type: "object",
33                properties: {
34                    words: {
35                        type: "boolean",
36                        default: true
37                    },
38                    nonwords: {
39                        type: "boolean",
40                        default: false
41                    },
42                    overrides: {
43                        type: "object",
44                        additionalProperties: {
45                            type: "boolean"
46                        }
47                    }
48                },
49                additionalProperties: false
50            }
51        ],
52        messages: {
53            unexpectedBefore: "Unexpected space before unary operator '{{operator}}'.",
54            unexpectedAfter: "Unexpected space after unary operator '{{operator}}'.",
55            unexpectedAfterWord: "Unexpected space after unary word operator '{{word}}'.",
56            wordOperator: "Unary word operator '{{word}}' must be followed by whitespace.",
57            operator: "Unary operator '{{operator}}' must be followed by whitespace.",
58            beforeUnaryExpressions: "Space is required before unary expressions '{{token}}'."
59        }
60    },
61
62    create(context) {
63        const options = context.options[0] || { words: true, nonwords: false };
64
65        const sourceCode = context.getSourceCode();
66
67        //--------------------------------------------------------------------------
68        // Helpers
69        //--------------------------------------------------------------------------
70
71        /**
72         * Check if the node is the first "!" in a "!!" convert to Boolean expression
73         * @param {ASTnode} node AST node
74         * @returns {boolean} Whether or not the node is first "!" in "!!"
75         */
76        function isFirstBangInBangBangExpression(node) {
77            return node && node.type === "UnaryExpression" && node.argument.operator === "!" &&
78                node.argument && node.argument.type === "UnaryExpression" && node.argument.operator === "!";
79        }
80
81        /**
82         * Checks if an override exists for a given operator.
83         * @param {string} operator Operator
84         * @returns {boolean} Whether or not an override has been provided for the operator
85         */
86        function overrideExistsForOperator(operator) {
87            return options.overrides && Object.prototype.hasOwnProperty.call(options.overrides, operator);
88        }
89
90        /**
91         * Gets the value that the override was set to for this operator
92         * @param {string} operator Operator
93         * @returns {boolean} Whether or not an override enforces a space with this operator
94         */
95        function overrideEnforcesSpaces(operator) {
96            return options.overrides[operator];
97        }
98
99        /**
100         * Verify Unary Word Operator has spaces after the word operator
101         * @param {ASTnode} node AST node
102         * @param {Object} firstToken first token from the AST node
103         * @param {Object} secondToken second token from the AST node
104         * @param {string} word The word to be used for reporting
105         * @returns {void}
106         */
107        function verifyWordHasSpaces(node, firstToken, secondToken, word) {
108            if (secondToken.range[0] === firstToken.range[1]) {
109                context.report({
110                    node,
111                    messageId: "wordOperator",
112                    data: {
113                        word
114                    },
115                    fix(fixer) {
116                        return fixer.insertTextAfter(firstToken, " ");
117                    }
118                });
119            }
120        }
121
122        /**
123         * Verify Unary Word Operator doesn't have spaces after the word operator
124         * @param {ASTnode} node AST node
125         * @param {Object} firstToken first token from the AST node
126         * @param {Object} secondToken second token from the AST node
127         * @param {string} word The word to be used for reporting
128         * @returns {void}
129         */
130        function verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word) {
131            if (astUtils.canTokensBeAdjacent(firstToken, secondToken)) {
132                if (secondToken.range[0] > firstToken.range[1]) {
133                    context.report({
134                        node,
135                        messageId: "unexpectedAfterWord",
136                        data: {
137                            word
138                        },
139                        fix(fixer) {
140                            return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
141                        }
142                    });
143                }
144            }
145        }
146
147        /**
148         * Check Unary Word Operators for spaces after the word operator
149         * @param {ASTnode} node AST node
150         * @param {Object} firstToken first token from the AST node
151         * @param {Object} secondToken second token from the AST node
152         * @param {string} word The word to be used for reporting
153         * @returns {void}
154         */
155        function checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, word) {
156            if (overrideExistsForOperator(word)) {
157                if (overrideEnforcesSpaces(word)) {
158                    verifyWordHasSpaces(node, firstToken, secondToken, word);
159                } else {
160                    verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word);
161                }
162            } else if (options.words) {
163                verifyWordHasSpaces(node, firstToken, secondToken, word);
164            } else {
165                verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word);
166            }
167        }
168
169        /**
170         * Verifies YieldExpressions satisfy spacing requirements
171         * @param {ASTnode} node AST node
172         * @returns {void}
173         */
174        function checkForSpacesAfterYield(node) {
175            const tokens = sourceCode.getFirstTokens(node, 3),
176                word = "yield";
177
178            if (!node.argument || node.delegate) {
179                return;
180            }
181
182            checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], word);
183        }
184
185        /**
186         * Verifies AwaitExpressions satisfy spacing requirements
187         * @param {ASTNode} node AwaitExpression AST node
188         * @returns {void}
189         */
190        function checkForSpacesAfterAwait(node) {
191            const tokens = sourceCode.getFirstTokens(node, 3);
192
193            checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], "await");
194        }
195
196        /**
197         * Verifies UnaryExpression, UpdateExpression and NewExpression have spaces before or after the operator
198         * @param {ASTnode} node AST node
199         * @param {Object} firstToken First token in the expression
200         * @param {Object} secondToken Second token in the expression
201         * @returns {void}
202         */
203        function verifyNonWordsHaveSpaces(node, firstToken, secondToken) {
204            if (node.prefix) {
205                if (isFirstBangInBangBangExpression(node)) {
206                    return;
207                }
208                if (firstToken.range[1] === secondToken.range[0]) {
209                    context.report({
210                        node,
211                        messageId: "operator",
212                        data: {
213                            operator: firstToken.value
214                        },
215                        fix(fixer) {
216                            return fixer.insertTextAfter(firstToken, " ");
217                        }
218                    });
219                }
220            } else {
221                if (firstToken.range[1] === secondToken.range[0]) {
222                    context.report({
223                        node,
224                        messageId: "beforeUnaryExpressions",
225                        data: {
226                            token: secondToken.value
227                        },
228                        fix(fixer) {
229                            return fixer.insertTextBefore(secondToken, " ");
230                        }
231                    });
232                }
233            }
234        }
235
236        /**
237         * Verifies UnaryExpression, UpdateExpression and NewExpression don't have spaces before or after the operator
238         * @param {ASTnode} node AST node
239         * @param {Object} firstToken First token in the expression
240         * @param {Object} secondToken Second token in the expression
241         * @returns {void}
242         */
243        function verifyNonWordsDontHaveSpaces(node, firstToken, secondToken) {
244            if (node.prefix) {
245                if (secondToken.range[0] > firstToken.range[1]) {
246                    context.report({
247                        node,
248                        messageId: "unexpectedAfter",
249                        data: {
250                            operator: firstToken.value
251                        },
252                        fix(fixer) {
253                            if (astUtils.canTokensBeAdjacent(firstToken, secondToken)) {
254                                return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
255                            }
256                            return null;
257                        }
258                    });
259                }
260            } else {
261                if (secondToken.range[0] > firstToken.range[1]) {
262                    context.report({
263                        node,
264                        messageId: "unexpectedBefore",
265                        data: {
266                            operator: secondToken.value
267                        },
268                        fix(fixer) {
269                            return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
270                        }
271                    });
272                }
273            }
274        }
275
276        /**
277         * Verifies UnaryExpression, UpdateExpression and NewExpression satisfy spacing requirements
278         * @param {ASTnode} node AST node
279         * @returns {void}
280         */
281        function checkForSpaces(node) {
282            const tokens = node.type === "UpdateExpression" && !node.prefix
283                ? sourceCode.getLastTokens(node, 2)
284                : sourceCode.getFirstTokens(node, 2);
285            const firstToken = tokens[0];
286            const secondToken = tokens[1];
287
288            if ((node.type === "NewExpression" || node.prefix) && firstToken.type === "Keyword") {
289                checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, firstToken.value);
290                return;
291            }
292
293            const operator = node.prefix ? tokens[0].value : tokens[1].value;
294
295            if (overrideExistsForOperator(operator)) {
296                if (overrideEnforcesSpaces(operator)) {
297                    verifyNonWordsHaveSpaces(node, firstToken, secondToken);
298                } else {
299                    verifyNonWordsDontHaveSpaces(node, firstToken, secondToken);
300                }
301            } else if (options.nonwords) {
302                verifyNonWordsHaveSpaces(node, firstToken, secondToken);
303            } else {
304                verifyNonWordsDontHaveSpaces(node, firstToken, secondToken);
305            }
306        }
307
308        //--------------------------------------------------------------------------
309        // Public
310        //--------------------------------------------------------------------------
311
312        return {
313            UnaryExpression: checkForSpaces,
314            UpdateExpression: checkForSpaces,
315            NewExpression: checkForSpaces,
316            YieldExpression: checkForSpacesAfterYield,
317            AwaitExpression: checkForSpacesAfterAwait
318        };
319
320    }
321};
322