Repository: prettier/prettier (JavaScript)

Issue:
Unnecessary leading semi
**Prettier 2.1.0**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAeeBbADgGwIbwB8AOlAARmoBuAtBnlmQAI4R4Amc7AvMSABT1G3MgBI4VBDACUZAGRyyASyhKYAWQb9pfQqgD0tISSgHMuAnBMgANCAhYYS6AGdkoPACdPEAO4AFLwQ3FDwqCCV2WxAAI088MABrOBgAZSwElQBzZBhPAFc4OzgMGK5OdgAZPCgs-LwsuAAxCE96GCda5BA8fJgIaIALGAwcAHVBtTgXDLA4VOC1JSo1AE9usBc3OxUXOE8Yf3is+mQAMzwcPbsAKxcADwAheKSU1LwMOEqVOHPL65Ad3uqWyODgAEV8hB4H8rkUQBlPHtPN0qIVolhPCoYGNIjBBsgABwABjsmIgezG8Sw3Ux032kmiAEcofAjg4Qj0XDQoHAuFxop44CylEKjg1TkgLnC7HsMEpYQCXKCIazflL-vCYHgYrj2PjkAAmOx5PBKHDZADCEAwkpAUGgvzs+T2ABUdSFpQC0XAAJJQTiwVJgLGOACCAdSMFWYMVcAAvvGgA)
```sh
--arrow-parens avoid
--parser vue
--no-semi
--trailing-comma none
```

**Input:**
```vue
<template>
  <v-map @loaded="(map = $event) && initMap()"></v-map>
</template>

```

**Output:**
```vue
<template>
  <v-map @loaded=";(map = $event) && initMap()"></v-map>
</template>

```

**Expected behavior:**
```vue
<template>
  <v-map @loaded="(map = $event) && initMap()"></v-map>
</template>
```


Hints:
This is expected and we format js like this, when `--no-semi`, but looks weird in this case.
Yes, when code like `;(() => {...})()` is placed at start of js file, the leading semi is unnecessary too, can this behavior be adjusted?
We won't change current behavior in js files, but this one may worth discuss.
__updated:__

Here is another interesting case:

<details>
<summary>ASI after if statement</summary>

**Prettier 2.1.0**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBLAZgAgBQEMCUmwAOlJpgEbb6kC+ppeAzgJ5RjWYC8AfEaeVwB3XKhiZc1ARJFjKUqLXzUQAGhAQADjFTQmyULgBORiEIAKxhPpS4ANiJb71FI7jABrODADKm96hQAObIMEYArnDqcAC2FHAAJgmJADK4weG4QXAAYhBGMbgwOsHIILjhMBBqIAAWMDF2AOq1YnBM-mBwPtZiqABuYixlYEzOaFBMcEYw5m5Bhcjo9lPqAFZMAB4AQm6e3j64MXApgXBLK1EgG5s+gUF2cACK4RDwF3arIP5GU0ZlFFw8TsNU0RkCMCaqASMFqyAAHAAGdRgiBTJpuTRlMHtab9c7qACOr3gcy0NnKTAAtFA4IlEjUjHBiagmXMsoskMtPlcpjFUB8vkx7o8Xm9zlzLuoYECoTC4UgAEzStyoOz3ADCEBinJA7QArDVwlMACpAmzcr79SIASSgyVgPjA4O0AEF7T4YCxHoK4LRaEA)
```sh
--parser babel
--no-semi
```

**Input:**
```jsx
if (a) {
  b()
}

(async() => {
  await a()
  await b()
})()
```

**Output:**
```jsx
if (a) {
  b()
}

;(async () => {
  await a()
  await b()
})()

```
</details>

but yes, I think no change, see my comment below.
> maybe we need to protect against the programmer adding code before the expression statement in the future

Yes, we won't change it in js files. The behavior for js  file  is  out scope of  this issue.
> We won't change current behavior in js files

Here is the rationale, linked from the `--no-semi` option: https://prettier.io/docs/en/rationale.html#semicolons

(My apologies for the other comment. Yes this may be off-topic now, but I think the link is relevant to answer the question in <https://github.com/prettier/prettier/issues/9060#issuecomment-680431266>.)

> this one may worth discuss

It would definitely be nice to be consistent with Angular & HTML parsers, which would format that template unchanged.
--no-semi

Input:

```
[1, 2].forEach((item) => console.log(item))

[1, 2].forEach((item) => console.log(item))
```

Output:

```
;[1, 2]
  .forEach((item) => console.log(item))

  [(1, 2)].forEach((item) => console.log(item))
```

The first line, I can understand.

The second line, I'm shocked.
Run 

```js
console.log(
[1, 2].forEach((item) => console.log(item))

[1, 2].forEach((item) => console.log(item))
)
```

and

```js
[1, 2].map((item) => item)

[0, 1].toFixed(10)
```

to understand it.
> Run
> 
> ```js
> console.log(
> [1, 2].forEach((item) => console.log(item))
> 
> [1, 2].forEach((item) => console.log(item))
> )
> ```
> 
> and
> 
> ```js
> [1, 2].map((item) => item)
> 
> [0, 1].toFixed(10)
> ```
> 
> to understand it.

Why is it wrong
If you still can't get it, parser your code on https://astexplorer.net/, and check the AST.
Please reconsider, leading semicolon is invalid in Alpine.js. Currently need to ignore every such node.
已收到。谢谢，我会尽快回复你。

Patch:
diff --git a/src/language-js/print/expression-statement.js b/src/language-js/print/expression-statement.js
new file mode 100644
--- /dev/null
+++ b/src/language-js/print/expression-statement.js
@@ -0,0 +1,44 @@
+import { printDanglingComments } from "../../main/comments/print.js";
+import {
+  markerForIfWithoutBlockAndSameLineComment,
+  hasComment,
+  CommentCheckFlags,
+} from "../utils/index.js";
+import {
+  isSingleJsxExpressionStatementInMarkdown,
+  isSingleVueEventBindingExpressionStatement,
+  isVueEventBindingExpression,
+} from "./semicolon.js";
+
+function printExpressionStatement(path, options, print) {
+  const parts = [print("expression")];
+
+  if (isSingleVueEventBindingExpressionStatement(path, options)) {
+    if (isVueEventBindingExpression(path.node.expression)) {
+      parts.push(";");
+    }
+  } else if (isSingleJsxExpressionStatementInMarkdown(path, options)) {
+    // Do not append semicolon after the only JSX element in a program
+  } else if (options.semi) {
+    parts.push(";");
+  }
+
+  if (
+    hasComment(
+      path.node,
+      CommentCheckFlags.Dangling,
+      ({ marker }) => marker === markerForIfWithoutBlockAndSameLineComment
+    )
+  ) {
+    parts.push(
+      " ",
+      printDanglingComments(path, options, {
+        marker: markerForIfWithoutBlockAndSameLineComment,
+      })
+    );
+  }
+
+  return parts;
+}
+
+export { printExpressionStatement };
diff --git a/src/language-js/print/html-binding.js b/src/language-js/print/html-binding.js
--- a/src/language-js/print/html-binding.js
+++ b/src/language-js/print/html-binding.js
@@ -32,22 +32,4 @@ function printHtmlBinding(path, options, print) {
   }
 }
 
-// based on https://github.com/prettier/prettier/blob/main/src/language-html/syntax-vue.js isVueEventBindingExpression()
-function isVueEventBindingExpression(node) {
-  switch (node.type) {
-    case "MemberExpression":
-      switch (node.property.type) {
-        case "Identifier":
-        case "NumericLiteral":
-        case "StringLiteral":
-          return isVueEventBindingExpression(node.object);
-      }
-      return false;
-    case "Identifier":
-      return true;
-    default:
-      return false;
-  }
-}
-
-export { isVueEventBindingExpression, printHtmlBinding };
+export { printHtmlBinding };
diff --git a/src/language-js/print/semicolon.js b/src/language-js/print/semicolon.js
--- a/src/language-js/print/semicolon.js
+++ b/src/language-js/print/semicolon.js
@@ -3,12 +3,15 @@ import {
   getLeftSidePathName,
   hasNakedLeftSide,
   isJsxElement,
-  isTheOnlyJsxElementInMarkdown,
 } from "../utils/index.js";
 import { shouldPrintParamsWithoutParens } from "./function.js";
 
 function shouldPrintLeadingSemicolon(path, options) {
-  if (options.semi || isTheOnlyJsxElementInMarkdown(options, path)) {
+  if (
+    options.semi ||
+    isSingleJsxExpressionStatementInMarkdown(path, options) ||
+    isSingleVueEventBindingExpressionStatement(path, options)
+  ) {
     return false;
   }
 
@@ -86,4 +89,47 @@ function expressionNeedsASIProtection(path, options) {
   );
 }
 
-export { shouldPrintLeadingSemicolon };
+function isSingleJsxExpressionStatementInMarkdown({ node, parent }, options) {
+  return (
+    (options.parentParser === "markdown" || options.parentParser === "mdx") &&
+    node.type === "ExpressionStatement" &&
+    isJsxElement(node.expression) &&
+    parent.type === "Program" &&
+    parent.body.length === 1
+  );
+}
+
+// based on /src/language-html/syntax-vue.js isVueEventBindingExpression()
+function isVueEventBindingExpression(node) {
+  switch (node.type) {
+    case "MemberExpression":
+      switch (node.property.type) {
+        case "Identifier":
+        case "NumericLiteral":
+        case "StringLiteral":
+          return isVueEventBindingExpression(node.object);
+      }
+      return false;
+    case "Identifier":
+      return true;
+    default:
+      return false;
+  }
+}
+
+function isSingleVueEventBindingExpressionStatement({ node, parent }, options) {
+  return (
+    (options.parser === "__vue_event_binding" ||
+      options.parser === "__vue_ts_event_binding") &&
+    node.type === "ExpressionStatement" &&
+    parent.type === "Program" &&
+    parent.body.length === 1
+  );
+}
+
+export {
+  shouldPrintLeadingSemicolon,
+  isSingleJsxExpressionStatementInMarkdown,
+  isSingleVueEventBindingExpressionStatement,
+  isVueEventBindingExpression,
+};
diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js
--- a/src/language-js/printer-estree.js
+++ b/src/language-js/printer-estree.js
@@ -18,12 +18,10 @@ import pathNeedsParens from "./needs-parens.js";
 import {
   hasComment,
   CommentCheckFlags,
-  isTheOnlyJsxElementInMarkdown,
   isNextLineEmpty,
   needsHardlineAfterDanglingComment,
   isCallExpression,
   isMemberExpression,
-  markerForIfWithoutBlockAndSameLineComment,
   isArrayOrTupleExpression,
   isObjectOrRecordExpression,
   startsWithNoLookaheadToken,
@@ -33,10 +31,7 @@ import { locStart, locEnd } from "./loc.js";
 import isBlockComment from "./utils/is-block-comment.js";
 import isIgnored from "./utils/is-ignored.js";
 
-import {
-  printHtmlBinding,
-  isVueEventBindingExpression,
-} from "./print/html-binding.js";
+import { printHtmlBinding } from "./print/html-binding.js";
 import { printAngular } from "./print/angular.js";
 import { printJsx } from "./print/jsx.js";
 import { printFlow } from "./print/flow.js";
@@ -88,6 +83,7 @@ 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";
+import { printExpressionStatement } from "./print/expression-statement.js";
 
 /**
  * @typedef {import("../common/ast-path.js").default} AstPath
@@ -197,35 +193,8 @@ function printPathNoParens(path, options, print, args) {
     // Babel extension.
     case "EmptyStatement":
       return "";
-    case "ExpressionStatement": {
-      if (
-        options.parser === "__vue_event_binding" ||
-        options.parser === "__vue_ts_event_binding"
-      ) {
-        const { parent } = path;
-        if (
-          parent.type === "Program" &&
-          parent.body.length === 1 &&
-          parent.body[0] === node
-        ) {
-          return [
-            print("expression"),
-            isVueEventBindingExpression(node.expression) ? ";" : "",
-          ];
-        }
-      }
-
-      const danglingComment = printDanglingComments(path, options, {
-        marker: markerForIfWithoutBlockAndSameLineComment,
-      });
-
-      // Do not append semicolon after the only JSX element in a program
-      return [
-        print("expression"),
-        isTheOnlyJsxElementInMarkdown(options, path) ? "" : semi,
-        danglingComment ? [" ", danglingComment] : "",
-      ];
-    }
+    case "ExpressionStatement":
+      return printExpressionStatement(path, options, print);
 
     case "ChainExpression":
       return print("expression");
diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js
--- a/src/language-js/utils/index.js
+++ b/src/language-js/utils/index.js
@@ -244,22 +244,6 @@ function isAngularTestWrapper(node) {
  */
 const isJsxElement = createTypeCheckFunction(["JSXElement", "JSXFragment"]);
 
-function isTheOnlyJsxElementInMarkdown(options, path) {
-  if (options.parentParser !== "markdown" && options.parentParser !== "mdx") {
-    return false;
-  }
-
-  const { node } = path;
-
-  if (!node.expression || !isJsxElement(node.expression)) {
-    return false;
-  }
-
-  const { parent } = path;
-
-  return parent.type === "Program" && parent.body.length === 1;
-}
-
 function isGetterOrSetter(node) {
   return node.kind === "get" || node.kind === "set";
 }
@@ -1181,7 +1165,6 @@ export {
   isStringPropSafeToUnquote,
   isTemplateOnItsOwnLine,
   isTestCall,
-  isTheOnlyJsxElementInMarkdown,
   isTSXFile,
   isTypeAnnotationAFunction,
   isNextLineEmpty,


Test patch:
diff --git a/tests/format/vue/ts-event-binding/__snapshots__/jsfmt.spec.js.snap b/tests/format/vue/event-binding/__snapshots__/jsfmt.spec.js.snap
similarity index 95%
rename from tests/format/vue/ts-event-binding/__snapshots__/jsfmt.spec.js.snap
rename to tests/format/vue/event-binding/__snapshots__/jsfmt.spec.js.snap
--- a/tests/format/vue/ts-event-binding/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/vue/event-binding/__snapshots__/jsfmt.spec.js.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`basic.vue format 1`] = `
+exports[`basic-ts.vue format 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
 printWidth: 80
@@ -42,7 +42,7 @@ function log(...args) {
 ================================================================================
 `;
 
-exports[`function-expression.vue format 1`] = `
+exports[`function-expression-ts.vue format 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
 printWidth: 80
diff --git a/tests/format/vue/ts-event-binding/basic.vue b/tests/format/vue/event-binding/basic-ts.vue
similarity index 100%
rename from tests/format/vue/ts-event-binding/basic.vue
rename to tests/format/vue/event-binding/basic-ts.vue
diff --git a/tests/format/vue/ts-event-binding/function-expression.vue b/tests/format/vue/event-binding/function-expression-ts.vue
similarity index 100%
rename from tests/format/vue/ts-event-binding/function-expression.vue
rename to tests/format/vue/event-binding/function-expression-ts.vue
diff --git a/tests/format/vue/ts-event-binding/jsfmt.spec.js b/tests/format/vue/event-binding/jsfmt.spec.js
similarity index 100%
rename from tests/format/vue/ts-event-binding/jsfmt.spec.js
rename to tests/format/vue/event-binding/jsfmt.spec.js
diff --git a/tests/format/vue/event-binding/no-semi/__snapshots__/jsfmt.spec.js.snap b/tests/format/vue/event-binding/no-semi/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
--- /dev/null
+++ b/tests/format/vue/event-binding/no-semi/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`single-expression.vue - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <foo-bar
+    @click="[foo, bar].forEach(fn => void fn())"
+    @loaded="(map = $event) && initMap()"
+  ></foo-bar>
+</template>
+
+=====================================output=====================================
+<template>
+  <foo-bar
+    @click="[foo, bar].forEach((fn) => void fn())"
+    @loaded="(map = $event) && initMap()"
+  ></foo-bar>
+</template>
+
+================================================================================
+`;
diff --git a/tests/format/vue/event-binding/no-semi/jsfmt.spec.js b/tests/format/vue/event-binding/no-semi/jsfmt.spec.js
new file mode 100644
--- /dev/null
+++ b/tests/format/vue/event-binding/no-semi/jsfmt.spec.js
@@ -0,0 +1 @@
+run_spec(import.meta, ["vue"], { semi: false });
diff --git a/tests/format/vue/event-binding/no-semi/single-expression.vue b/tests/format/vue/event-binding/no-semi/single-expression.vue
new file mode 100644
--- /dev/null
+++ b/tests/format/vue/event-binding/no-semi/single-expression.vue
@@ -0,0 +1,6 @@
+<template>
+  <foo-bar
+    @click="[foo, bar].forEach(fn => void fn())"
+    @loaded="(map = $event) && initMap()"
+  ></foo-bar>
+</template>