UNPKG

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