• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to enforce spacing before and after keywords.
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("./utils/ast-utils"),
13    keywords = require("./utils/keywords");
14
15//------------------------------------------------------------------------------
16// Constants
17//------------------------------------------------------------------------------
18
19const PREV_TOKEN = /^[)\]}>]$/u;
20const NEXT_TOKEN = /^(?:[([{<~!]|\+\+?|--?)$/u;
21const PREV_TOKEN_M = /^[)\]}>*]$/u;
22const NEXT_TOKEN_M = /^[{*]$/u;
23const TEMPLATE_OPEN_PAREN = /\$\{$/u;
24const TEMPLATE_CLOSE_PAREN = /^\}/u;
25const CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template)$/u;
26const KEYS = keywords.concat(["as", "async", "await", "from", "get", "let", "of", "set", "yield"]);
27
28// check duplications.
29(function() {
30    KEYS.sort();
31    for (let i = 1; i < KEYS.length; ++i) {
32        if (KEYS[i] === KEYS[i - 1]) {
33            throw new Error(`Duplication was found in the keyword list: ${KEYS[i]}`);
34        }
35    }
36}());
37
38//------------------------------------------------------------------------------
39// Helpers
40//------------------------------------------------------------------------------
41
42/**
43 * Checks whether or not a given token is a "Template" token ends with "${".
44 * @param {Token} token A token to check.
45 * @returns {boolean} `true` if the token is a "Template" token ends with "${".
46 */
47function isOpenParenOfTemplate(token) {
48    return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value);
49}
50
51/**
52 * Checks whether or not a given token is a "Template" token starts with "}".
53 * @param {Token} token A token to check.
54 * @returns {boolean} `true` if the token is a "Template" token starts with "}".
55 */
56function isCloseParenOfTemplate(token) {
57    return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value);
58}
59
60//------------------------------------------------------------------------------
61// Rule Definition
62//------------------------------------------------------------------------------
63
64module.exports = {
65    meta: {
66        type: "layout",
67
68        docs: {
69            description: "enforce consistent spacing before and after keywords",
70            category: "Stylistic Issues",
71            recommended: false,
72            url: "https://eslint.org/docs/rules/keyword-spacing"
73        },
74
75        fixable: "whitespace",
76
77        schema: [
78            {
79                type: "object",
80                properties: {
81                    before: { type: "boolean", default: true },
82                    after: { type: "boolean", default: true },
83                    overrides: {
84                        type: "object",
85                        properties: KEYS.reduce((retv, key) => {
86                            retv[key] = {
87                                type: "object",
88                                properties: {
89                                    before: { type: "boolean" },
90                                    after: { type: "boolean" }
91                                },
92                                additionalProperties: false
93                            };
94                            return retv;
95                        }, {}),
96                        additionalProperties: false
97                    }
98                },
99                additionalProperties: false
100            }
101        ],
102        messages: {
103            expectedBefore: "Expected space(s) before \"{{value}}\".",
104            expectedAfter: "Expected space(s) after \"{{value}}\".",
105            unexpectedBefore: "Unexpected space(s) before \"{{value}}\".",
106            unexpectedAfter: "Unexpected space(s) after \"{{value}}\"."
107        }
108    },
109
110    create(context) {
111        const sourceCode = context.getSourceCode();
112
113        /**
114         * Reports a given token if there are not space(s) before the token.
115         * @param {Token} token A token to report.
116         * @param {RegExp} pattern A pattern of the previous token to check.
117         * @returns {void}
118         */
119        function expectSpaceBefore(token, pattern) {
120            const prevToken = sourceCode.getTokenBefore(token);
121
122            if (prevToken &&
123                (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
124                !isOpenParenOfTemplate(prevToken) &&
125                astUtils.isTokenOnSameLine(prevToken, token) &&
126                !sourceCode.isSpaceBetweenTokens(prevToken, token)
127            ) {
128                context.report({
129                    loc: token.loc,
130                    messageId: "expectedBefore",
131                    data: token,
132                    fix(fixer) {
133                        return fixer.insertTextBefore(token, " ");
134                    }
135                });
136            }
137        }
138
139        /**
140         * Reports a given token if there are space(s) before the token.
141         * @param {Token} token A token to report.
142         * @param {RegExp} pattern A pattern of the previous token to check.
143         * @returns {void}
144         */
145        function unexpectSpaceBefore(token, pattern) {
146            const prevToken = sourceCode.getTokenBefore(token);
147
148            if (prevToken &&
149                (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
150                !isOpenParenOfTemplate(prevToken) &&
151                astUtils.isTokenOnSameLine(prevToken, token) &&
152                sourceCode.isSpaceBetweenTokens(prevToken, token)
153            ) {
154                context.report({
155                    loc: { start: prevToken.loc.end, end: token.loc.start },
156                    messageId: "unexpectedBefore",
157                    data: token,
158                    fix(fixer) {
159                        return fixer.removeRange([prevToken.range[1], token.range[0]]);
160                    }
161                });
162            }
163        }
164
165        /**
166         * Reports a given token if there are not space(s) after the token.
167         * @param {Token} token A token to report.
168         * @param {RegExp} pattern A pattern of the next token to check.
169         * @returns {void}
170         */
171        function expectSpaceAfter(token, pattern) {
172            const nextToken = sourceCode.getTokenAfter(token);
173
174            if (nextToken &&
175                (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
176                !isCloseParenOfTemplate(nextToken) &&
177                astUtils.isTokenOnSameLine(token, nextToken) &&
178                !sourceCode.isSpaceBetweenTokens(token, nextToken)
179            ) {
180                context.report({
181                    loc: token.loc,
182                    messageId: "expectedAfter",
183                    data: token,
184                    fix(fixer) {
185                        return fixer.insertTextAfter(token, " ");
186                    }
187                });
188            }
189        }
190
191        /**
192         * Reports a given token if there are space(s) after the token.
193         * @param {Token} token A token to report.
194         * @param {RegExp} pattern A pattern of the next token to check.
195         * @returns {void}
196         */
197        function unexpectSpaceAfter(token, pattern) {
198            const nextToken = sourceCode.getTokenAfter(token);
199
200            if (nextToken &&
201                (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
202                !isCloseParenOfTemplate(nextToken) &&
203                astUtils.isTokenOnSameLine(token, nextToken) &&
204                sourceCode.isSpaceBetweenTokens(token, nextToken)
205            ) {
206
207                context.report({
208                    loc: { start: token.loc.end, end: nextToken.loc.start },
209                    messageId: "unexpectedAfter",
210                    data: token,
211                    fix(fixer) {
212                        return fixer.removeRange([token.range[1], nextToken.range[0]]);
213                    }
214                });
215            }
216        }
217
218        /**
219         * Parses the option object and determines check methods for each keyword.
220         * @param {Object|undefined} options The option object to parse.
221         * @returns {Object} - Normalized option object.
222         *      Keys are keywords (there are for every keyword).
223         *      Values are instances of `{"before": function, "after": function}`.
224         */
225        function parseOptions(options = {}) {
226            const before = options.before !== false;
227            const after = options.after !== false;
228            const defaultValue = {
229                before: before ? expectSpaceBefore : unexpectSpaceBefore,
230                after: after ? expectSpaceAfter : unexpectSpaceAfter
231            };
232            const overrides = (options && options.overrides) || {};
233            const retv = Object.create(null);
234
235            for (let i = 0; i < KEYS.length; ++i) {
236                const key = KEYS[i];
237                const override = overrides[key];
238
239                if (override) {
240                    const thisBefore = ("before" in override) ? override.before : before;
241                    const thisAfter = ("after" in override) ? override.after : after;
242
243                    retv[key] = {
244                        before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore,
245                        after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter
246                    };
247                } else {
248                    retv[key] = defaultValue;
249                }
250            }
251
252            return retv;
253        }
254
255        const checkMethodMap = parseOptions(context.options[0]);
256
257        /**
258         * Reports a given token if usage of spacing followed by the token is
259         * invalid.
260         * @param {Token} token A token to report.
261         * @param {RegExp} [pattern] Optional. A pattern of the previous
262         *      token to check.
263         * @returns {void}
264         */
265        function checkSpacingBefore(token, pattern) {
266            checkMethodMap[token.value].before(token, pattern || PREV_TOKEN);
267        }
268
269        /**
270         * Reports a given token if usage of spacing preceded by the token is
271         * invalid.
272         * @param {Token} token A token to report.
273         * @param {RegExp} [pattern] Optional. A pattern of the next
274         *      token to check.
275         * @returns {void}
276         */
277        function checkSpacingAfter(token, pattern) {
278            checkMethodMap[token.value].after(token, pattern || NEXT_TOKEN);
279        }
280
281        /**
282         * Reports a given token if usage of spacing around the token is invalid.
283         * @param {Token} token A token to report.
284         * @returns {void}
285         */
286        function checkSpacingAround(token) {
287            checkSpacingBefore(token);
288            checkSpacingAfter(token);
289        }
290
291        /**
292         * Reports the first token of a given node if the first token is a keyword
293         * and usage of spacing around the token is invalid.
294         * @param {ASTNode|null} node A node to report.
295         * @returns {void}
296         */
297        function checkSpacingAroundFirstToken(node) {
298            const firstToken = node && sourceCode.getFirstToken(node);
299
300            if (firstToken && firstToken.type === "Keyword") {
301                checkSpacingAround(firstToken);
302            }
303        }
304
305        /**
306         * Reports the first token of a given node if the first token is a keyword
307         * and usage of spacing followed by the token is invalid.
308         *
309         * This is used for unary operators (e.g. `typeof`), `function`, and `super`.
310         * Other rules are handling usage of spacing preceded by those keywords.
311         * @param {ASTNode|null} node A node to report.
312         * @returns {void}
313         */
314        function checkSpacingBeforeFirstToken(node) {
315            const firstToken = node && sourceCode.getFirstToken(node);
316
317            if (firstToken && firstToken.type === "Keyword") {
318                checkSpacingBefore(firstToken);
319            }
320        }
321
322        /**
323         * Reports the previous token of a given node if the token is a keyword and
324         * usage of spacing around the token is invalid.
325         * @param {ASTNode|null} node A node to report.
326         * @returns {void}
327         */
328        function checkSpacingAroundTokenBefore(node) {
329            if (node) {
330                const token = sourceCode.getTokenBefore(node, astUtils.isKeywordToken);
331
332                checkSpacingAround(token);
333            }
334        }
335
336        /**
337         * Reports `async` or `function` keywords of a given node if usage of
338         * spacing around those keywords is invalid.
339         * @param {ASTNode} node A node to report.
340         * @returns {void}
341         */
342        function checkSpacingForFunction(node) {
343            const firstToken = node && sourceCode.getFirstToken(node);
344
345            if (firstToken &&
346                ((firstToken.type === "Keyword" && firstToken.value === "function") ||
347                firstToken.value === "async")
348            ) {
349                checkSpacingBefore(firstToken);
350            }
351        }
352
353        /**
354         * Reports `class` and `extends` keywords of a given node if usage of
355         * spacing around those keywords is invalid.
356         * @param {ASTNode} node A node to report.
357         * @returns {void}
358         */
359        function checkSpacingForClass(node) {
360            checkSpacingAroundFirstToken(node);
361            checkSpacingAroundTokenBefore(node.superClass);
362        }
363
364        /**
365         * Reports `if` and `else` keywords of a given node if usage of spacing
366         * around those keywords is invalid.
367         * @param {ASTNode} node A node to report.
368         * @returns {void}
369         */
370        function checkSpacingForIfStatement(node) {
371            checkSpacingAroundFirstToken(node);
372            checkSpacingAroundTokenBefore(node.alternate);
373        }
374
375        /**
376         * Reports `try`, `catch`, and `finally` keywords of a given node if usage
377         * of spacing around those keywords is invalid.
378         * @param {ASTNode} node A node to report.
379         * @returns {void}
380         */
381        function checkSpacingForTryStatement(node) {
382            checkSpacingAroundFirstToken(node);
383            checkSpacingAroundFirstToken(node.handler);
384            checkSpacingAroundTokenBefore(node.finalizer);
385        }
386
387        /**
388         * Reports `do` and `while` keywords of a given node if usage of spacing
389         * around those keywords is invalid.
390         * @param {ASTNode} node A node to report.
391         * @returns {void}
392         */
393        function checkSpacingForDoWhileStatement(node) {
394            checkSpacingAroundFirstToken(node);
395            checkSpacingAroundTokenBefore(node.test);
396        }
397
398        /**
399         * Reports `for` and `in` keywords of a given node if usage of spacing
400         * around those keywords is invalid.
401         * @param {ASTNode} node A node to report.
402         * @returns {void}
403         */
404        function checkSpacingForForInStatement(node) {
405            checkSpacingAroundFirstToken(node);
406            checkSpacingAroundTokenBefore(node.right);
407        }
408
409        /**
410         * Reports `for` and `of` keywords of a given node if usage of spacing
411         * around those keywords is invalid.
412         * @param {ASTNode} node A node to report.
413         * @returns {void}
414         */
415        function checkSpacingForForOfStatement(node) {
416            if (node.await) {
417                checkSpacingBefore(sourceCode.getFirstToken(node, 0));
418                checkSpacingAfter(sourceCode.getFirstToken(node, 1));
419            } else {
420                checkSpacingAroundFirstToken(node);
421            }
422            checkSpacingAround(sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken));
423        }
424
425        /**
426         * Reports `import`, `export`, `as`, and `from` keywords of a given node if
427         * usage of spacing around those keywords is invalid.
428         *
429         * This rule handles the `*` token in module declarations.
430         *
431         *     import*as A from "./a"; /*error Expected space(s) after "import".
432         *                               error Expected space(s) before "as".
433         * @param {ASTNode} node A node to report.
434         * @returns {void}
435         */
436        function checkSpacingForModuleDeclaration(node) {
437            const firstToken = sourceCode.getFirstToken(node);
438
439            checkSpacingBefore(firstToken, PREV_TOKEN_M);
440            checkSpacingAfter(firstToken, NEXT_TOKEN_M);
441
442            if (node.type === "ExportDefaultDeclaration") {
443                checkSpacingAround(sourceCode.getTokenAfter(firstToken));
444            }
445
446            if (node.type === "ExportAllDeclaration" && node.exported) {
447                const asToken = sourceCode.getTokenBefore(node.exported);
448
449                checkSpacingBefore(asToken, PREV_TOKEN_M);
450            }
451
452            if (node.source) {
453                const fromToken = sourceCode.getTokenBefore(node.source);
454
455                checkSpacingBefore(fromToken, PREV_TOKEN_M);
456                checkSpacingAfter(fromToken, NEXT_TOKEN_M);
457            }
458        }
459
460        /**
461         * Reports `as` keyword of a given node if usage of spacing around this
462         * keyword is invalid.
463         * @param {ASTNode} node A node to report.
464         * @returns {void}
465         */
466        function checkSpacingForImportNamespaceSpecifier(node) {
467            const asToken = sourceCode.getFirstToken(node, 1);
468
469            checkSpacingBefore(asToken, PREV_TOKEN_M);
470        }
471
472        /**
473         * Reports `static`, `get`, and `set` keywords of a given node if usage of
474         * spacing around those keywords is invalid.
475         * @param {ASTNode} node A node to report.
476         * @returns {void}
477         */
478        function checkSpacingForProperty(node) {
479            if (node.static) {
480                checkSpacingAroundFirstToken(node);
481            }
482            if (node.kind === "get" ||
483                node.kind === "set" ||
484                (
485                    (node.method || node.type === "MethodDefinition") &&
486                    node.value.async
487                )
488            ) {
489                const token = sourceCode.getTokenBefore(
490                    node.key,
491                    tok => {
492                        switch (tok.value) {
493                            case "get":
494                            case "set":
495                            case "async":
496                                return true;
497                            default:
498                                return false;
499                        }
500                    }
501                );
502
503                if (!token) {
504                    throw new Error("Failed to find token get, set, or async beside method name");
505                }
506
507
508                checkSpacingAround(token);
509            }
510        }
511
512        /**
513         * Reports `await` keyword of a given node if usage of spacing before
514         * this keyword is invalid.
515         * @param {ASTNode} node A node to report.
516         * @returns {void}
517         */
518        function checkSpacingForAwaitExpression(node) {
519            checkSpacingBefore(sourceCode.getFirstToken(node));
520        }
521
522        return {
523
524            // Statements
525            DebuggerStatement: checkSpacingAroundFirstToken,
526            WithStatement: checkSpacingAroundFirstToken,
527
528            // Statements - Control flow
529            BreakStatement: checkSpacingAroundFirstToken,
530            ContinueStatement: checkSpacingAroundFirstToken,
531            ReturnStatement: checkSpacingAroundFirstToken,
532            ThrowStatement: checkSpacingAroundFirstToken,
533            TryStatement: checkSpacingForTryStatement,
534
535            // Statements - Choice
536            IfStatement: checkSpacingForIfStatement,
537            SwitchStatement: checkSpacingAroundFirstToken,
538            SwitchCase: checkSpacingAroundFirstToken,
539
540            // Statements - Loops
541            DoWhileStatement: checkSpacingForDoWhileStatement,
542            ForInStatement: checkSpacingForForInStatement,
543            ForOfStatement: checkSpacingForForOfStatement,
544            ForStatement: checkSpacingAroundFirstToken,
545            WhileStatement: checkSpacingAroundFirstToken,
546
547            // Statements - Declarations
548            ClassDeclaration: checkSpacingForClass,
549            ExportNamedDeclaration: checkSpacingForModuleDeclaration,
550            ExportDefaultDeclaration: checkSpacingForModuleDeclaration,
551            ExportAllDeclaration: checkSpacingForModuleDeclaration,
552            FunctionDeclaration: checkSpacingForFunction,
553            ImportDeclaration: checkSpacingForModuleDeclaration,
554            VariableDeclaration: checkSpacingAroundFirstToken,
555
556            // Expressions
557            ArrowFunctionExpression: checkSpacingForFunction,
558            AwaitExpression: checkSpacingForAwaitExpression,
559            ClassExpression: checkSpacingForClass,
560            FunctionExpression: checkSpacingForFunction,
561            NewExpression: checkSpacingBeforeFirstToken,
562            Super: checkSpacingBeforeFirstToken,
563            ThisExpression: checkSpacingBeforeFirstToken,
564            UnaryExpression: checkSpacingBeforeFirstToken,
565            YieldExpression: checkSpacingBeforeFirstToken,
566
567            // Others
568            ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
569            MethodDefinition: checkSpacingForProperty,
570            Property: checkSpacingForProperty
571        };
572    }
573};
574