diff --git a/src/language-js/print/semicolon.js b/src/language-js/print/semicolon.js new file mode 100644 index 000000000..5a4e6d0ea --- /dev/null +++ b/src/language-js/print/semicolon.js @@ -0,0 +1,89 @@ +import pathNeedsParens from "../needs-parens.js"; +import { + getLeftSidePathName, + hasNakedLeftSide, + isJsxElement, + isTheOnlyJsxElementInMarkdown, +} from "../utils/index.js"; +import { shouldPrintParamsWithoutParens } from "./function.js"; + +function shouldPrintLeadingSemicolon(path, options) { + if (options.semi || isTheOnlyJsxElementInMarkdown(options, path)) { + return false; + } + + const { node, key, parent } = path; + if ( + node.type === "ExpressionStatement" && + // `Program.directives` don't need leading semicolon + ((key === "body" && + (parent.type === "Program" || + parent.type === "BlockStatement" || + parent.type === "StaticBlock" || + parent.type === "TSModuleBlock")) || + (key === "consequent" && parent.type === "SwitchCase")) && + path.call(() => expressionNeedsASIProtection(path, options), "expression") + ) { + return true; + } + + return false; +} + +function expressionNeedsASIProtection(path, options) { + const { node } = path; + switch (node.type) { + case "ParenthesizedExpression": + case "TypeCastExpression": + case "ArrayExpression": + case "ArrayPattern": + case "TemplateLiteral": + case "TemplateElement": + case "RegExpLiteral": + return true; + case "ArrowFunctionExpression": + if (!shouldPrintParamsWithoutParens(path, options)) { + return true; + } + break; + + case "UnaryExpression": { + const { prefix, operator } = node; + if (prefix && (operator === "+" || operator === "-")) { + return true; + } + break; + } + case "BindExpression": + if (!node.object) { + return true; + } + break; + + case "Literal": + if (node.regex) { + return true; + } + break; + + default: + if (isJsxElement(node)) { + return true; + } + } + + if (pathNeedsParens(path, options)) { + return true; + } + + if (!hasNakedLeftSide(node)) { + return false; + } + + return path.call( + () => expressionNeedsASIProtection(path, options), + ...getLeftSidePathName(node) + ); +} + +export { shouldPrintLeadingSemicolon }; diff --git a/src/language-js/print/statement.js b/src/language-js/print/statement.js index bdd2adbbb..6c67cd5ec 100644 --- a/src/language-js/print/statement.js +++ b/src/language-js/print/statement.js @@ -1,21 +1,18 @@ import { hardline } from "../../document/builders.js"; -import pathNeedsParens from "../needs-parens.js"; -import { - getLeftSidePathName, - hasNakedLeftSide, - isJsxElement, - isTheOnlyJsxElementInMarkdown, - hasComment, - CommentCheckFlags, - isNextLineEmpty, -} from "../utils/index.js"; -import { shouldPrintParamsWithoutParens } from "./function.js"; +import { isNextLineEmpty } from "../utils/index.js"; /** * @typedef {import("../../document/builders.js").Doc} Doc * @typedef {import("../../common/ast-path.js")} AstPath */ +/* +- `Program` ("directives" and "body") +- `BlockStatement` +- `StaticBlock` +- `SwitchCase` ("consequent") +- `TSModuleBlock` (TypeScript) +*/ function printStatementSequence(path, options, print, property) { const { node } = path; const parts = []; @@ -28,21 +25,7 @@ function printStatementSequence(path, options, print, property) { return; } - const needsSemi = - !options.semi && - !isTheOnlyJsxElementInMarkdown(options, path) && - statementNeedsASIProtection(path, options); - - // in no-semi mode, prepend statement with semicolon if it might break ASI - // don't prepend the only JSX element in a program with semicolon - if (needsSemi && hasComment(node, CommentCheckFlags.Leading)) { - parts.push(print([], { needsSemi: true })); - } else { - if (needsSemi) { - parts.push(";"); - } - parts.push(print()); - } + parts.push(print()); if (node !== lastStatement) { parts.push(hardline); @@ -65,73 +48,4 @@ function getLastStatement(statements) { } } -function statementNeedsASIProtection(path, options) { - const { node } = path; - - if (node.type !== "ExpressionStatement") { - return false; - } - - return path.call( - (childPath) => expressionNeedsASIProtection(childPath, options), - "expression" - ); -} - -function expressionNeedsASIProtection(path, options) { - const { node } = path; - switch (node.type) { - case "ParenthesizedExpression": - case "TypeCastExpression": - case "ArrayExpression": - case "ArrayPattern": - case "TemplateLiteral": - case "TemplateElement": - case "RegExpLiteral": - return true; - case "ArrowFunctionExpression": - if (!shouldPrintParamsWithoutParens(path, options)) { - return true; - } - break; - - case "UnaryExpression": { - const { prefix, operator } = node; - if (prefix && (operator === "+" || operator === "-")) { - return true; - } - break; - } - case "BindExpression": - if (!node.object) { - return true; - } - break; - - case "Literal": - if (node.regex) { - return true; - } - break; - - default: - if (isJsxElement(node)) { - return true; - } - } - - if (pathNeedsParens(path, options)) { - return true; - } - - if (!hasNakedLeftSide(node)) { - return false; - } - - return path.call( - () => expressionNeedsASIProtection(path, options), - ...getLeftSidePathName(node) - ); -} - export { printStatementSequence }; diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 6bd115d10..d2c0014f9 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -88,6 +88,7 @@ import { printBlock, printBlockBody } from "./print/block.js"; import { printLiteral } from "./print/literal.js"; import { printDecorators } from "./print/decorators.js"; import { printTypeAnnotationProperty } from "./print/type-annotation.js"; +import { shouldPrintLeadingSemicolon } from "./print/semicolon.js"; /** * @typedef {import("../common/ast-path.js").default} AstPath @@ -143,7 +144,7 @@ function genericPrint(path, options, print, args) { } const needsParens = pathNeedsParens(path, options); - const needsSemi = args?.needsSemi; + const needsSemi = shouldPrintLeadingSemicolon(path, options); if (!needsParens) { if (needsSemi) {