2 * @fileoverview Rule to flag missing semicolons.
3 * @author Nicholas C. Zakas
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const FixTracker = require("./utils/fix-tracker");
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
23 description: "require or disallow semicolons instead of ASI",
24 category: "Stylistic Issues",
26 url: "https://eslint.org/docs/rules/semi"
42 beforeStatementContinuationChars: {
43 enum: ["always", "any", "never"]
46 additionalProperties: false
61 omitLastInOneLineBlock: { type: "boolean" }
63 additionalProperties: false
73 missingSemi: "Missing semicolon.",
74 extraSemi: "Extra semicolon."
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();
87 //--------------------------------------------------------------------------
89 //--------------------------------------------------------------------------
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.
97 function report(node, missing) {
98 const lastToken = sourceCode.getLastToken(node);
104 messageId = "missingSemi";
106 start: lastToken.loc.end,
107 end: astUtils.getNextLocation(sourceCode, lastToken.loc.end)
109 fix = function(fixer) {
110 return fixer.insertTextAfter(lastToken, ";");
113 messageId = "extraSemi";
115 fix = function(fixer) {
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
122 return new FixTracker(fixer, sourceCode)
123 .retainSurroundingTokens(lastToken)
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 `}`.
142 function isRedundantSemi(semiToken) {
143 const nextToken = sourceCode.getTokenAfter(semiToken);
147 astUtils.isClosingBraceToken(nextToken) ||
148 astUtils.isSemicolonToken(nextToken)
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.
157 function isEndOfArrowBlock(lastToken) {
158 if (!astUtils.isClosingBraceToken(lastToken)) {
161 const node = sourceCode.getNodeByRangeIndex(lastToken.range[0]);
164 node.type === "BlockStatement" &&
165 node.parent.type === "ArrowFunctionExpression"
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.
174 function isOnSameLineWithNextToken(node) {
175 const prevToken = sourceCode.getLastToken(node, 1);
176 const nextToken = sourceCode.getTokenAfter(node);
178 return !!nextToken && astUtils.isTokenOnSameLine(prevToken, nextToken);
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.
186 function maybeAsiHazardAfter(node) {
189 if (t === "DoWhileStatement" ||
190 t === "BreakStatement" ||
191 t === "ContinueStatement" ||
192 t === "DebuggerStatement" ||
193 t === "ImportDeclaration" ||
194 t === "ExportAllDeclaration"
198 if (t === "ReturnStatement") {
199 return Boolean(node.argument);
201 if (t === "ExportNamedDeclaration") {
202 return Boolean(node.declaration);
204 if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) {
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 `--`.
216 function maybeAsiHazardBefore(token) {
219 OPT_OUT_PATTERN.test(token.value) &&
220 token.value !== "++" &&
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.
232 function canRemoveSemicolon(node) {
233 if (isRedundantSemi(sourceCode.getLastToken(node))) {
234 return true; // `;;` or `;}`
236 if (isOnSameLineWithNextToken(node)) {
237 return false; // One liner.
239 if (beforeStatementContinuationChars === "never" && !maybeAsiHazardAfter(node)) {
240 return true; // ASI works. This statement doesn't connect to the next.
242 if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
243 return true; // ASI works. The next token doesn't connect to this statement.
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.
254 function isOneLinerBlock(node) {
255 const parent = node.parent;
256 const nextToken = sourceCode.getTokenAfter(node);
258 if (!nextToken || nextToken.value !== "}") {
263 parent.type === "BlockStatement" &&
264 parent.loc.start.line === parent.loc.end.line
269 * Checks a node to see if it's followed by a semicolon.
270 * @param {ASTNode} node The node to check.
273 function checkForSemicolon(node) {
274 const isSemi = astUtils.isSemicolonToken(sourceCode.getLastToken(node));
277 if (isSemi && canRemoveSemicolon(node)) {
279 } else if (!isSemi && beforeStatementContinuationChars === "always" && maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
283 const oneLinerBlock = (exceptOneLine && isOneLinerBlock(node));
285 if (isSemi && oneLinerBlock) {
287 } else if (!isSemi && !oneLinerBlock) {
294 * Checks to see if there's a semicolon after a variable declaration.
295 * @param {ASTNode} node The node to check.
298 function checkForSemicolonForVariableDeclaration(node) {
299 const parent = node.parent;
301 if ((parent.type !== "ForStatement" || parent.init !== node) &&
302 (!/^For(?:In|Of)Statement/u.test(parent.type) || parent.left !== node)
304 checkForSemicolon(node);
308 //--------------------------------------------------------------------------
310 //--------------------------------------------------------------------------
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);
328 ExportDefaultDeclaration(node) {
329 if (!/(?:Class|Function)Declaration/u.test(node.declaration.type)) {
330 checkForSemicolon(node);