123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- /**
- * @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
- //------------------------------------------------------------------------------
- /** @type {import('../shared/types').Rule} */
- module.exports = {
- meta: {
- type: "layout",
- docs: {
- description: "Require or disallow semicolons instead of ASI",
- 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
- }
- ]
- },
- messages: {
- missingSemi: "Missing semicolon.",
- extraSemi: "Extra semicolon."
- }
- },
- create(context) {
- const OPT_OUT_PATTERN = /^[-[(/+`]/u; // One of [(/+-`
- const unsafeClassFieldNames = new Set(["get", "set", "static"]);
- const unsafeClassFieldFollowers = new Set(["*", "in", "instanceof"]);
- 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 messageId,
- fix,
- loc;
- if (!missing) {
- messageId = "missingSemi";
- loc = {
- start: lastToken.loc.end,
- end: astUtils.getNextLocation(sourceCode, lastToken.loc.end)
- };
- fix = function(fixer) {
- return fixer.insertTextAfter(lastToken, ";");
- };
- } else {
- messageId = "extraSemi";
- loc = lastToken.loc;
- 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,
- messageId,
- fix
- });
- }
- /**
- * Check whether a given semicolon token is redundant.
- * @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"
- );
- }
- /**
- * Checks if a given PropertyDefinition node followed by a semicolon
- * can safely remove that semicolon. It is not to safe to remove if
- * the class field name is "get", "set", or "static", or if
- * followed by a generator method.
- * @param {ASTNode} node The node to check.
- * @returns {boolean} `true` if the node cannot have the semicolon
- * removed.
- */
- function maybeClassFieldAsiHazard(node) {
- if (node.type !== "PropertyDefinition") {
- return false;
- }
- /*
- * Computed property names and non-identifiers are always safe
- * as they can be distinguished from keywords easily.
- */
- const needsNameCheck = !node.computed && node.key.type === "Identifier";
- /*
- * Certain names are problematic unless they also have a
- * a way to distinguish between keywords and property
- * names.
- */
- if (needsNameCheck && unsafeClassFieldNames.has(node.key.name)) {
- /*
- * Special case: If the field name is `static`,
- * it is only valid if the field is marked as static,
- * so "static static" is okay but "static" is not.
- */
- const isStaticStatic = node.static && node.key.name === "static";
- /*
- * For other unsafe names, we only care if there is no
- * initializer. No initializer = hazard.
- */
- if (!isStaticStatic && !node.value) {
- return true;
- }
- }
- const followingToken = sourceCode.getTokenAfter(node);
- return unsafeClassFieldFollowers.has(followingToken.value);
- }
- /**
- * 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 (maybeClassFieldAsiHazard(node)) {
- return false;
- }
- if (isOnSameLineWithNextToken(node)) {
- return false; // One liner.
- }
- // continuation characters should not apply to class fields
- if (
- node.type !== "PropertyDefinition" &&
- 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 the last item in a one-liner block.
- * Block is any `BlockStatement` or `StaticBlock` node. Block is a one-liner if its
- * braces (and consequently everything between them) are on the same line.
- * @param {ASTNode} node The node to check.
- * @returns {boolean} whether the node is the last item in a one-liner block.
- */
- function isLastInOneLinerBlock(node) {
- const parent = node.parent;
- const nextToken = sourceCode.getTokenAfter(node);
- if (!nextToken || nextToken.value !== "}") {
- return false;
- }
- if (parent.type === "BlockStatement") {
- return parent.loc.start.line === parent.loc.end.line;
- }
- if (parent.type === "StaticBlock") {
- const openingBrace = sourceCode.getFirstToken(parent, { skip: 1 }); // skip the `static` token
- return openingBrace.loc.start.line === parent.loc.end.line;
- }
- return false;
- }
- /**
- * 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" &&
- node.type !== "PropertyDefinition" &&
- maybeAsiHazardBefore(sourceCode.getTokenAfter(node))
- ) {
- report(node);
- }
- } else {
- const oneLinerBlock = (exceptOneLine && isLastInOneLinerBlock(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);
- }
- },
- PropertyDefinition: checkForSemicolon
- };
- }
- };
|