• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to flag missing semicolons.
3 * @author Nicholas C. Zakas
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const FixTracker = require("./utils/fix-tracker");
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: "require or disallow semicolons instead of ASI",
24            category: "Stylistic Issues",
25            recommended: false,
26            url: "https://eslint.org/docs/rules/semi"
27        },
28
29        fixable: "code",
30
31        schema: {
32            anyOf: [
33                {
34                    type: "array",
35                    items: [
36                        {
37                            enum: ["never"]
38                        },
39                        {
40                            type: "object",
41                            properties: {
42                                beforeStatementContinuationChars: {
43                                    enum: ["always", "any", "never"]
44                                }
45                            },
46                            additionalProperties: false
47                        }
48                    ],
49                    minItems: 0,
50                    maxItems: 2
51                },
52                {
53                    type: "array",
54                    items: [
55                        {
56                            enum: ["always"]
57                        },
58                        {
59                            type: "object",
60                            properties: {
61                                omitLastInOneLineBlock: { type: "boolean" }
62                            },
63                            additionalProperties: false
64                        }
65                    ],
66                    minItems: 0,
67                    maxItems: 2
68                }
69            ]
70        },
71
72        messages: {
73            missingSemi: "Missing semicolon.",
74            extraSemi: "Extra semicolon."
75        }
76    },
77
78    create(context) {
79
80        const OPT_OUT_PATTERN = /^[-[(/+`]/u; // One of [(/+-`
81        const options = context.options[1];
82        const never = context.options[0] === "never";
83        const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock);
84        const beforeStatementContinuationChars = options && options.beforeStatementContinuationChars || "any";
85        const sourceCode = context.getSourceCode();
86
87        //--------------------------------------------------------------------------
88        // Helpers
89        //--------------------------------------------------------------------------
90
91        /**
92         * Reports a semicolon error with appropriate location and message.
93         * @param {ASTNode} node The node with an extra or missing semicolon.
94         * @param {boolean} missing True if the semicolon is missing.
95         * @returns {void}
96         */
97        function report(node, missing) {
98            const lastToken = sourceCode.getLastToken(node);
99            let messageId,
100                fix,
101                loc;
102
103            if (!missing) {
104                messageId = "missingSemi";
105                loc = {
106                    start: lastToken.loc.end,
107                    end: astUtils.getNextLocation(sourceCode, lastToken.loc.end)
108                };
109                fix = function(fixer) {
110                    return fixer.insertTextAfter(lastToken, ";");
111                };
112            } else {
113                messageId = "extraSemi";
114                loc = lastToken.loc;
115                fix = function(fixer) {
116
117                    /*
118                     * Expand the replacement range to include the surrounding
119                     * tokens to avoid conflicting with no-extra-semi.
120                     * https://github.com/eslint/eslint/issues/7928
121                     */
122                    return new FixTracker(fixer, sourceCode)
123                        .retainSurroundingTokens(lastToken)
124                        .remove(lastToken);
125                };
126            }
127
128            context.report({
129                node,
130                loc,
131                messageId,
132                fix
133            });
134
135        }
136
137        /**
138         * Check whether a given semicolon token is redundant.
139         * @param {Token} semiToken A semicolon token to check.
140         * @returns {boolean} `true` if the next token is `;` or `}`.
141         */
142        function isRedundantSemi(semiToken) {
143            const nextToken = sourceCode.getTokenAfter(semiToken);
144
145            return (
146                !nextToken ||
147                astUtils.isClosingBraceToken(nextToken) ||
148                astUtils.isSemicolonToken(nextToken)
149            );
150        }
151
152        /**
153         * Check whether a given token is the closing brace of an arrow function.
154         * @param {Token} lastToken A token to check.
155         * @returns {boolean} `true` if the token is the closing brace of an arrow function.
156         */
157        function isEndOfArrowBlock(lastToken) {
158            if (!astUtils.isClosingBraceToken(lastToken)) {
159                return false;
160            }
161            const node = sourceCode.getNodeByRangeIndex(lastToken.range[0]);
162
163            return (
164                node.type === "BlockStatement" &&
165                node.parent.type === "ArrowFunctionExpression"
166            );
167        }
168
169        /**
170         * Check whether a given node is on the same line with the next token.
171         * @param {Node} node A statement node to check.
172         * @returns {boolean} `true` if the node is on the same line with the next token.
173         */
174        function isOnSameLineWithNextToken(node) {
175            const prevToken = sourceCode.getLastToken(node, 1);
176            const nextToken = sourceCode.getTokenAfter(node);
177
178            return !!nextToken && astUtils.isTokenOnSameLine(prevToken, nextToken);
179        }
180
181        /**
182         * Check whether a given node can connect the next line if the next line is unreliable.
183         * @param {Node} node A statement node to check.
184         * @returns {boolean} `true` if the node can connect the next line.
185         */
186        function maybeAsiHazardAfter(node) {
187            const t = node.type;
188
189            if (t === "DoWhileStatement" ||
190                t === "BreakStatement" ||
191                t === "ContinueStatement" ||
192                t === "DebuggerStatement" ||
193                t === "ImportDeclaration" ||
194                t === "ExportAllDeclaration"
195            ) {
196                return false;
197            }
198            if (t === "ReturnStatement") {
199                return Boolean(node.argument);
200            }
201            if (t === "ExportNamedDeclaration") {
202                return Boolean(node.declaration);
203            }
204            if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) {
205                return false;
206            }
207
208            return true;
209        }
210
211        /**
212         * Check whether a given token can connect the previous statement.
213         * @param {Token} token A token to check.
214         * @returns {boolean} `true` if the token is one of `[`, `(`, `/`, `+`, `-`, ```, `++`, and `--`.
215         */
216        function maybeAsiHazardBefore(token) {
217            return (
218                Boolean(token) &&
219                OPT_OUT_PATTERN.test(token.value) &&
220                token.value !== "++" &&
221                token.value !== "--"
222            );
223        }
224
225        /**
226         * Check if the semicolon of a given node is unnecessary, only true if:
227         *   - next token is a valid statement divider (`;` or `}`).
228         *   - next token is on a new line and the node is not connectable to the new line.
229         * @param {Node} node A statement node to check.
230         * @returns {boolean} whether the semicolon is unnecessary.
231         */
232        function canRemoveSemicolon(node) {
233            if (isRedundantSemi(sourceCode.getLastToken(node))) {
234                return true; // `;;` or `;}`
235            }
236            if (isOnSameLineWithNextToken(node)) {
237                return false; // One liner.
238            }
239            if (beforeStatementContinuationChars === "never" && !maybeAsiHazardAfter(node)) {
240                return true; // ASI works. This statement doesn't connect to the next.
241            }
242            if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
243                return true; // ASI works. The next token doesn't connect to this statement.
244            }
245
246            return false;
247        }
248
249        /**
250         * Checks a node to see if it's in a one-liner block statement.
251         * @param {ASTNode} node The node to check.
252         * @returns {boolean} whether the node is in a one-liner block statement.
253         */
254        function isOneLinerBlock(node) {
255            const parent = node.parent;
256            const nextToken = sourceCode.getTokenAfter(node);
257
258            if (!nextToken || nextToken.value !== "}") {
259                return false;
260            }
261            return (
262                !!parent &&
263                parent.type === "BlockStatement" &&
264                parent.loc.start.line === parent.loc.end.line
265            );
266        }
267
268        /**
269         * Checks a node to see if it's followed by a semicolon.
270         * @param {ASTNode} node The node to check.
271         * @returns {void}
272         */
273        function checkForSemicolon(node) {
274            const isSemi = astUtils.isSemicolonToken(sourceCode.getLastToken(node));
275
276            if (never) {
277                if (isSemi && canRemoveSemicolon(node)) {
278                    report(node, true);
279                } else if (!isSemi && beforeStatementContinuationChars === "always" && maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
280                    report(node);
281                }
282            } else {
283                const oneLinerBlock = (exceptOneLine && isOneLinerBlock(node));
284
285                if (isSemi && oneLinerBlock) {
286                    report(node, true);
287                } else if (!isSemi && !oneLinerBlock) {
288                    report(node);
289                }
290            }
291        }
292
293        /**
294         * Checks to see if there's a semicolon after a variable declaration.
295         * @param {ASTNode} node The node to check.
296         * @returns {void}
297         */
298        function checkForSemicolonForVariableDeclaration(node) {
299            const parent = node.parent;
300
301            if ((parent.type !== "ForStatement" || parent.init !== node) &&
302                (!/^For(?:In|Of)Statement/u.test(parent.type) || parent.left !== node)
303            ) {
304                checkForSemicolon(node);
305            }
306        }
307
308        //--------------------------------------------------------------------------
309        // Public API
310        //--------------------------------------------------------------------------
311
312        return {
313            VariableDeclaration: checkForSemicolonForVariableDeclaration,
314            ExpressionStatement: checkForSemicolon,
315            ReturnStatement: checkForSemicolon,
316            ThrowStatement: checkForSemicolon,
317            DoWhileStatement: checkForSemicolon,
318            DebuggerStatement: checkForSemicolon,
319            BreakStatement: checkForSemicolon,
320            ContinueStatement: checkForSemicolon,
321            ImportDeclaration: checkForSemicolon,
322            ExportAllDeclaration: checkForSemicolon,
323            ExportNamedDeclaration(node) {
324                if (!node.declaration) {
325                    checkForSemicolon(node);
326                }
327            },
328            ExportDefaultDeclaration(node) {
329                if (!/(?:Class|Function)Declaration/u.test(node.declaration.type)) {
330                    checkForSemicolon(node);
331                }
332            }
333        };
334
335    }
336};
337