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