Permalink
Cannot retrieve contributors at this time
| /** | |
| * @fileoverview Rule to flag missing semicolons. | |
| * @author Nicholas C. Zakas | |
| */ | |
| "use strict"; | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
| const FixTracker = require("./utils/fix-tracker"); | |
| const astUtils = require("./utils/ast-utils"); | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
| module.exports = { | |
| meta: { | |
| type: "layout", | |
| docs: { | |
| description: "require or disallow semicolons instead of ASI", | |
| category: "Stylistic Issues", | |
| recommended: false, | |
| url: "https://eslint.org/docs/rules/semi" | |
| }, | |
| fixable: "code", | |
| schema: { | |
| anyOf: [ | |
| { | |
| type: "array", | |
| items: [ | |
| { | |
| enum: ["never"] | |
| }, | |
| { | |
| type: "object", | |
| properties: { | |
| beforeStatementContinuationChars: { | |
| enum: ["always", "any", "never"] | |
| } | |
| }, | |
| additionalProperties: false | |
| } | |
| ], | |
| minItems: 0, | |
| maxItems: 2 | |
| }, | |
| { | |
| type: "array", | |
| items: [ | |
| { | |
| enum: ["always"] | |
| }, | |
| { | |
| type: "object", | |
| properties: { | |
| omitLastInOneLineBlock: { type: "boolean" } | |
| }, | |
| additionalProperties: false | |
| } | |
| ], | |
| minItems: 0, | |
| maxItems: 2 | |
| } | |
| ] | |
| } | |
| }, | |
| create(context) { | |
| const OPT_OUT_PATTERN = /^[-[(/+`]/u; // One of [(/+-` | |
| const options = context.options[1]; | |
| const never = context.options[0] === "never"; | |
| const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock); | |
| const beforeStatementContinuationChars = options && options.beforeStatementContinuationChars || "any"; | |
| const sourceCode = context.getSourceCode(); | |
| //-------------------------------------------------------------------------- | |
| // Helpers | |
| //-------------------------------------------------------------------------- | |
| /** | |
| * Reports a semicolon error with appropriate location and message. | |
| * @param {ASTNode} node The node with an extra or missing semicolon. | |
| * @param {boolean} missing True if the semicolon is missing. | |
| * @returns {void} | |
| */ | |
| function report(node, missing) { | |
| const lastToken = sourceCode.getLastToken(node); | |
| let message, | |
| fix, | |
| loc = lastToken.loc; | |
| if (!missing) { | |
| message = "Missing semicolon."; | |
| loc = loc.end; | |
| fix = function(fixer) { | |
| return fixer.insertTextAfter(lastToken, ";"); | |
| }; | |
| } else { | |
| message = "Extra semicolon."; | |
| loc = loc.start; | |
| fix = function(fixer) { | |
| /* | |
| * Expand the replacement range to include the surrounding | |
| * tokens to avoid conflicting with no-extra-semi. | |
| * https://github.com/eslint/eslint/issues/7928 | |
| */ | |
| return new FixTracker(fixer, sourceCode) | |
| .retainSurroundingTokens(lastToken) | |
| .remove(lastToken); | |
| }; | |
| } | |
| context.report({ | |
| node, | |
| loc, | |
| message, | |
| fix | |
| }); | |
| } | |
| /** | |
| * Check whether a given semicolon token is redandant. | |
| * @param {Token} semiToken A semicolon token to check. | |
| * @returns {boolean} `true` if the next token is `;` or `}`. | |
| */ | |
| function isRedundantSemi(semiToken) { | |
| const nextToken = sourceCode.getTokenAfter(semiToken); | |
| return ( | |
| !nextToken || | |
| astUtils.isClosingBraceToken(nextToken) || | |
| astUtils.isSemicolonToken(nextToken) | |
| ); | |
| } | |
| /** | |
| * Check whether a given token is the closing brace of an arrow function. | |
| * @param {Token} lastToken A token to check. | |
| * @returns {boolean} `true` if the token is the closing brace of an arrow function. | |
| */ | |
| function isEndOfArrowBlock(lastToken) { | |
| if (!astUtils.isClosingBraceToken(lastToken)) { | |
| return false; | |
| } | |
| const node = sourceCode.getNodeByRangeIndex(lastToken.range[0]); | |
| return ( | |
| node.type === "BlockStatement" && | |
| node.parent.type === "ArrowFunctionExpression" | |
| ); | |
| } | |
| /** | |
| * Check whether a given node is on the same line with the next token. | |
| * @param {Node} node A statement node to check. | |
| * @returns {boolean} `true` if the node is on the same line with the next token. | |
| */ | |
| function isOnSameLineWithNextToken(node) { | |
| const prevToken = sourceCode.getLastToken(node, 1); | |
| const nextToken = sourceCode.getTokenAfter(node); | |
| return !!nextToken && astUtils.isTokenOnSameLine(prevToken, nextToken); | |
| } | |
| /** | |
| * Check whether a given node can connect the next line if the next line is unreliable. | |
| * @param {Node} node A statement node to check. | |
| * @returns {boolean} `true` if the node can connect the next line. | |
| */ | |
| function maybeAsiHazardAfter(node) { | |
| const t = node.type; | |
| if (t === "DoWhileStatement" || | |
| t === "BreakStatement" || | |
| t === "ContinueStatement" || | |
| t === "DebuggerStatement" || | |
| t === "ImportDeclaration" || | |
| t === "ExportAllDeclaration" | |
| ) { | |
| return false; | |
| } | |
| if (t === "ReturnStatement") { | |
| return Boolean(node.argument); | |
| } | |
| if (t === "ExportNamedDeclaration") { | |
| return Boolean(node.declaration); | |
| } | |
| if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Check whether a given token can connect the previous statement. | |
| * @param {Token} token A token to check. | |
| * @returns {boolean} `true` if the token is one of `[`, `(`, `/`, `+`, `-`, ```, `++`, and `--`. | |
| */ | |
| function maybeAsiHazardBefore(token) { | |
| return ( | |
| Boolean(token) && | |
| OPT_OUT_PATTERN.test(token.value) && | |
| token.value !== "++" && | |
| token.value !== "--" | |
| ); | |
| } | |
| /** | |
| * Check if the semicolon of a given node is unnecessary, only true if: | |
| * - next token is a valid statement divider (`;` or `}`). | |
| * - next token is on a new line and the node is not connectable to the new line. | |
| * @param {Node} node A statement node to check. | |
| * @returns {boolean} whether the semicolon is unnecessary. | |
| */ | |
| function canRemoveSemicolon(node) { | |
| if (isRedundantSemi(sourceCode.getLastToken(node))) { | |
| return true; // `;;` or `;}` | |
| } | |
| if (isOnSameLineWithNextToken(node)) { | |
| return false; // One liner. | |
| } | |
| if (beforeStatementContinuationChars === "never" && !maybeAsiHazardAfter(node)) { | |
| return true; // ASI works. This statement doesn't connect to the next. | |
| } | |
| if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) { | |
| return true; // ASI works. The next token doesn't connect to this statement. | |
| } | |
| return false; | |
| } | |
| /** | |
| * Checks a node to see if it's in a one-liner block statement. | |
| * @param {ASTNode} node The node to check. | |
| * @returns {boolean} whether the node is in a one-liner block statement. | |
| */ | |
| function isOneLinerBlock(node) { | |
| const parent = node.parent; | |
| const nextToken = sourceCode.getTokenAfter(node); | |
| if (!nextToken || nextToken.value !== "}") { | |
| return false; | |
| } | |
| return ( | |
| !!parent && | |
| parent.type === "BlockStatement" && | |
| parent.loc.start.line === parent.loc.end.line | |
| ); | |
| } | |
| /** | |
| * Checks a node to see if it's followed by a semicolon. | |
| * @param {ASTNode} node The node to check. | |
| * @returns {void} | |
| */ | |
| function checkForSemicolon(node) { | |
| const isSemi = astUtils.isSemicolonToken(sourceCode.getLastToken(node)); | |
| if (never) { | |
| if (isSemi && canRemoveSemicolon(node)) { | |
| report(node, true); | |
| } else if (!isSemi && beforeStatementContinuationChars === "always" && maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) { | |
| report(node); | |
| } | |
| } else { | |
| const oneLinerBlock = (exceptOneLine && isOneLinerBlock(node)); | |
| if (isSemi && oneLinerBlock) { | |
| report(node, true); | |
| } else if (!isSemi && !oneLinerBlock) { | |
| report(node); | |
| } | |
| } | |
| } | |
| /** | |
| * Checks to see if there's a semicolon after a variable declaration. | |
| * @param {ASTNode} node The node to check. | |
| * @returns {void} | |
| */ | |
| function checkForSemicolonForVariableDeclaration(node) { | |
| const parent = node.parent; | |
| if ((parent.type !== "ForStatement" || parent.init !== node) && | |
| (!/^For(?:In|Of)Statement/u.test(parent.type) || parent.left !== node) | |
| ) { | |
| checkForSemicolon(node); | |
| } | |
| } | |
| //-------------------------------------------------------------------------- | |
| // Public API | |
| //-------------------------------------------------------------------------- | |
| return { | |
| VariableDeclaration: checkForSemicolonForVariableDeclaration, | |
| ExpressionStatement: checkForSemicolon, | |
| ReturnStatement: checkForSemicolon, | |
| ThrowStatement: checkForSemicolon, | |
| DoWhileStatement: checkForSemicolon, | |
| DebuggerStatement: checkForSemicolon, | |
| BreakStatement: checkForSemicolon, | |
| ContinueStatement: checkForSemicolon, | |
| ImportDeclaration: checkForSemicolon, | |
| ExportAllDeclaration: checkForSemicolon, | |
| ExportNamedDeclaration(node) { | |
| if (!node.declaration) { | |
| checkForSemicolon(node); | |
| } | |
| }, | |
| ExportDefaultDeclaration(node) { | |
| if (!/(?:Class|Function)Declaration/u.test(node.declaration.type)) { | |
| checkForSemicolon(node); | |
| } | |
| } | |
| }; | |
| } | |
| }; |