Repository: prettier/prettier (JavaScript) — Bug Fix

Issue:
[Vue] do not remove necessary semicolon in event bindings with --no-semi
**Prettier 1.15.2**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAeeBbADgGwIbwB8AOlAARmoAmAlgG5kACYONYA1gLzEhUQDKEDHBgALGlADmAbh6FUAelp0SURZlwE4hEABoQELDBrQAzslB4ATlYgB3AArWE5lHjoQaVPSABGVvA4RfixAiUlkGCsAVzh9URgMHAB1cXhTULA4fhcaYzo8gE9kcFNzfQlTOCsYBwDJDDxkADM8HCr9ACtTAA8AIQCgmH48YQAZCTgWto6Qbp7+cJw4AEVoiHhp9riQUKsqqxK6WJ8sKwkYZK8xZAAOAAZ9M4gq5ICsErO4A7op-Ss4ABHaI0AF1PANJpIVrbfRVDA0LazUxLVbrTbQmY7GB4XxXKg3JAAJn0UTwNFYUgAwkJGiUoNA-iBolUACq41wwqoAX25QA)
```sh
--parser vue
--no-semi
```

**Input:**
```vue
<template>
  <div @click="doSomething;"></div>
</template>
```

**Output:**
```vue
<template>
  <div @click="doSomething"></div>
</template>

```

**Expected behavior:**
Same as input. `doSomething` and `doSomething;` behave [differently](https://template-explorer.vuejs.org/#%3Cdiv%0A%20%20%40a%3D%22doSomething%22%0A%20%20%40b%3D%22doSomething%3B%22%0A%3E%3C%2Fdiv%3E%0A).


Discussion:
Note that this only applies to simple expressions, such as identifiers and member expressions, not call expressions, which always create the wrapper function.

Resolving patch:
diff --git a/src/common/internal-plugins.js b/src/common/internal-plugins.js
index d071a32d79f5..9be225ece530 100644
--- a/src/common/internal-plugins.js
+++ b/src/common/internal-plugins.js
@@ -33,6 +33,10 @@ module.exports = [
         return eval("require")("../language-js/parser-babylon").parsers
           .__vue_expression;
       },
+      get __vue_event_binding() {
+        return eval("require")("../language-js/parser-babylon").parsers
+          .__vue_event_binding;
+      },
       // JS - Flow
       get flow() {
         return eval("require")("../language-js/parser-flow").parsers.flow;
diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js
index 34c70c63e8db..b8611902b6e4 100644
--- a/src/language-html/printer-html.js
+++ b/src/language-html/printer-html.js
@@ -41,7 +41,11 @@ const {
 const preprocess = require("./preprocess");
 const assert = require("assert");
 const { insertPragma } = require("./pragma");
-const { printVueFor, printVueSlotScope } = require("./syntax-vue");
+const {
+  printVueFor,
+  printVueSlotScope,
+  isVueEventBindingExpression
+} = require("./syntax-vue");
 const { printImgSrcset } = require("./syntax-attribute");
 
 function concat(parts) {
@@ -942,17 +946,13 @@ function printEmbeddedAttributeValue(node, originalTextToDoc, options) {
     const jsExpressionBindingPatterns = ["^v-"];
 
     if (isKeyMatched(vueEventBindingPatterns)) {
-      // copied from https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/codegen/events.js#L3-L4
-      const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/;
-      const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/;
-
-      const value = getValue()
-        // https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/helpers.js#L104
-        .trim();
+      const value = getValue();
       return printMaybeHug(
-        simplePathRE.test(value) || fnExpRE.test(value)
+        isVueEventBindingExpression(value)
           ? textToDoc(value, { parser: "__js_expression" })
-          : stripTrailingHardline(textToDoc(value, { parser: "babylon" }))
+          : stripTrailingHardline(
+              textToDoc(value, { parser: "__vue_event_binding" })
+            )
       );
     }
 
diff --git a/src/language-html/syntax-vue.js b/src/language-html/syntax-vue.js
index 234bacaf0cb9..eb8698545633 100644
--- a/src/language-html/syntax-vue.js
+++ b/src/language-html/syntax-vue.js
@@ -66,7 +66,21 @@ function printVueSlotScope(value, textToDoc) {
   });
 }
 
+function isVueEventBindingExpression(eventBindingValue) {
+  // https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/codegen/events.js#L3-L4
+  // arrow function or anonymous function
+  const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/;
+  // simple member expression chain (a, a.b, a['b'], a["b"], a[0], a[b])
+  const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/;
+
+  // https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/helpers.js#L104
+  const value = eventBindingValue.trim();
+
+  return fnExpRE.test(value) || simplePathRE.test(value);
+}
+
 module.exports = {
+  isVueEventBindingExpression,
   printVueFor,
   printVueSlotScope
 };
diff --git a/src/language-js/html-binding.js b/src/language-js/html-binding.js
index 411a2e9988ad..0bcb25c173f9 100644
--- a/src/language-js/html-binding.js
+++ b/src/language-js/html-binding.js
@@ -45,6 +45,25 @@ function printHtmlBinding(path, options, print) {
   }
 }
 
+// based on https://github.com/prettier/prettier/blob/master/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;
+  }
+}
+
 module.exports = {
+  isVueEventBindingExpression,
   printHtmlBinding
 };
diff --git a/src/language-js/parser-babylon.js b/src/language-js/parser-babylon.js
index 00ad22e73e55..01402e55169b 100644
--- a/src/language-js/parser-babylon.js
+++ b/src/language-js/parser-babylon.js
@@ -41,44 +41,49 @@ function babylonOptions(extraOptions, extraPlugins) {
   );
 }
 
-function parse(text, parsers, opts) {
-  // Inline the require to avoid loading all the JS if we don't use it
-  const babylon = require("@babel/parser");
+function createParse(parseMethod) {
+  return (text, parsers, opts) => {
+    // Inline the require to avoid loading all the JS if we don't use it
+    const babylon = require("@babel/parser");
 
-  const combinations = [
-    babylonOptions({ strictMode: true }, ["decorators-legacy"]),
-    babylonOptions({ strictMode: false }, ["decorators-legacy"]),
-    babylonOptions({ strictMode: true }, [
-      ["decorators", { decoratorsBeforeExport: false }]
-    ]),
-    babylonOptions({ strictMode: false }, [
-      ["decorators", { decoratorsBeforeExport: false }]
-    ])
-  ];
+    const combinations = [
+      babylonOptions({ strictMode: true }, ["decorators-legacy"]),
+      babylonOptions({ strictMode: false }, ["decorators-legacy"]),
+      babylonOptions({ strictMode: true }, [
+        ["decorators", { decoratorsBeforeExport: false }]
+      ]),
+      babylonOptions({ strictMode: false }, [
+        ["decorators", { decoratorsBeforeExport: false }]
+      ])
+    ];
 
-  const parseMethod =
-    !opts || opts.parser === "babylon" ? "parse" : "parseExpression";
-
-  let ast;
-  try {
-    ast = tryCombinations(babylon[parseMethod].bind(null, text), combinations);
-  } catch (error) {
-    throw createError(
-      // babel error prints (l:c) with cols that are zero indexed
-      // so we need our custom error
-      error.message.replace(/ \(.*\)/, ""),
-      {
-        start: {
-          line: error.loc.line,
-          column: error.loc.column + 1
+    let ast;
+    try {
+      ast = tryCombinations(
+        babylon[parseMethod].bind(null, text),
+        combinations
+      );
+    } catch (error) {
+      throw createError(
+        // babel error prints (l:c) with cols that are zero indexed
+        // so we need our custom error
+        error.message.replace(/ \(.*\)/, ""),
+        {
+          start: {
+            line: error.loc.line,
+            column: error.loc.column + 1
+          }
         }
-      }
-    );
-  }
-  delete ast.tokens;
-  return postprocess(ast, Object.assign({}, opts, { originalText: text }));
+      );
+    }
+    delete ast.tokens;
+    return postprocess(ast, Object.assign({}, opts, { originalText: text }));
+  };
 }
 
+const parse = createParse("parse");
+const parseExpression = createParse("parseExpression");
+
 function tryCombinations(fn, combinations) {
   let error;
   for (let i = 0; i < combinations.length; i++) {
@@ -94,7 +99,7 @@ function tryCombinations(fn, combinations) {
 }
 
 function parseJson(text, parsers, opts) {
-  const ast = parse(text, parsers, Object.assign({}, opts, { parser: "json" }));
+  const ast = parseExpression(text, parsers, opts);
 
   ast.comments.forEach(assertJsonNode);
   assertJsonNode(ast);
@@ -164,17 +169,20 @@ const babylon = Object.assign(
   { parse, astFormat: "estree", hasPragma },
   locFns
 );
+const babylonExpression = Object.assign({}, babylon, {
+  parse: parseExpression
+});
 
 // Export as a plugin so we can reuse the same bundle for UMD loading
 module.exports = {
   parsers: {
     babylon,
-    json: Object.assign({}, babylon, {
+    json: Object.assign({}, babylonExpression, {
       hasPragma() {
         return true;
       }
     }),
-    json5: babylon,
+    json5: babylonExpression,
     "json-stringify": Object.assign(
       {
         parse: parseJson,
@@ -183,8 +191,10 @@ module.exports = {
       locFns
     ),
     /** @internal */
-    __js_expression: babylon,
+    __js_expression: babylonExpression,
     /** for vue filter */
-    __vue_expression: babylon
+    __vue_expression: babylonExpression,
+    /** for vue event binding to handle semicolon */
+    __vue_event_binding: babylon
   }
 };
diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js
index 72b654e8671c..117ec876dd98 100644
--- a/src/language-js/printer-estree.js
+++ b/src/language-js/printer-estree.js
@@ -34,7 +34,10 @@ const clean = require("./clean");
 const insertPragma = require("./pragma").insertPragma;
 const handleComments = require("./comments");
 const pathNeedsParens = require("./needs-parens");
-const { printHtmlBinding } = require("./html-binding");
+const {
+  printHtmlBinding,
+  isVueEventBindingExpression
+} = require("./html-binding");
 const preprocess = require("./preprocess");
 const {
   hasNode,
@@ -494,6 +497,21 @@ function printPathNoParens(path, options, print, args) {
       if (n.directive) {
         return concat([nodeStr(n.expression, options, true), semi]);
       }
+
+      if (options.parser === "__vue_event_binding") {
+        const parent = path.getParentNode();
+        if (
+          parent.type === "Program" &&
+          parent.body.length === 1 &&
+          parent.body[0] === n
+        ) {
+          return concat([
+            path.call(print, "expression"),
+            isVueEventBindingExpression(n.expression) ? ";" : ""
+          ]);
+        }
+      }
+
       // Do not append semicolon after the only JSX element in a program
       return concat([
         path.call(print, "expression"),
diff --git a/website/static/worker.js b/website/static/worker.js
index e51a2e6181bc..3bdae4ffd336 100644
--- a/website/static/worker.js
+++ b/website/static/worker.js
@@ -37,6 +37,10 @@ var parsers = {
     importScriptOnce("lib/parser-babylon.js");
     return prettierPlugins.babylon.parsers.__vue_expression;
   },
+  get __vue_event_binding() {
+    importScriptOnce("lib/parser-babylon.js");
+    return prettierPlugins.babylon.parsers.__vue_event_binding;
+  },
   // JS - Flow
   get flow() {
     importScriptOnce("lib/parser-flow.js");


Test patch:
diff --git a/tests/html_vue/__snapshots__/jsfmt.spec.js.snap b/tests/html_vue/__snapshots__/jsfmt.spec.js.snap
index 0aac6713767a..d3fd4f7a21f5 100644
--- a/tests/html_vue/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/html_vue/__snapshots__/jsfmt.spec.js.snap
@@ -37,6 +37,8 @@ printWidth: 80
       console.log(test);
     }
   "
+  @click="doSomething()"
+  @click="doSomething;"
 ></div>
 </template>
 
@@ -59,9 +61,9 @@ printWidth: 80
       return x;
     })"
     @click="/* hello */"
-    @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */; /* 5 */"
-    @click="$emit('click');"
-    @click="$emit('click');"
+    @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */ /* 5 */"
+    @click="$emit('click')"
+    @click="$emit('click')"
     @click="
       $emit('click');
       if (something) {
@@ -96,6 +98,8 @@ printWidth: 80
         console.log(test);
       }
     "
+    @click="doSomething()"
+    @click="doSomething;"
   ></div>
 </template>
 
@@ -140,6 +144,8 @@ trailingComma: "es5"
       console.log(test);
     }
   "
+  @click="doSomething()"
+  @click="doSomething;"
 ></div>
 </template>
 
@@ -162,9 +168,9 @@ trailingComma: "es5"
       return x;
     })"
     @click="/* hello */"
-    @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */; /* 5 */"
-    @click="$emit('click');"
-    @click="$emit('click');"
+    @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */ /* 5 */"
+    @click="$emit('click')"
+    @click="$emit('click')"
     @click="
       $emit('click');
       if (something) {
@@ -199,6 +205,115 @@ trailingComma: "es5"
         console.log(test);
       }
     "
+    @click="doSomething()"
+    @click="doSomething;"
+  ></div>
+</template>
+
+================================================================================
+`;
+
+exports[`attributes.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<template>
+<div
+  v-for="  item  in  items "
+  v-for="  item  of  items "
+  v-for="(    item    , index)    in    items"
+  v-for="value    in     object"
+  v-for="(value,    key)    in    object"
+  v-for="(value,    key)    of    object"
+  v-for="(value  ,   key,   index)    in   object"
+  v-for="   n    in   evenNumbers"
+  v-for=" n in even   ( numbers) "
+  v-for=" n    in    10"
+  v-for=" { a }    in    [0].map(()=>({a:1}))   "
+  v-for=" ({ a }, [c  ])    in    [0].map(()=>1)   "
+  v-for=" n in items.map(x => { return x }) "
+  @click="  /* hello */   "
+  @click="   /* 1 */ $emit( /* 2 */ 'click' /* 3 */ ) /* 4 */ ; /* 5 */   "
+  @click="   $emit(   'click'   )   "
+  @click="   $emit(   'click'   )  ;"
+  @click="   $emit(   'click'   )  ;if(something){for(let i=j;i<100;i++){}}else{}"
+  slot-scope="{destructuring:{a:{b}}}"
+  :class="{ longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true }"
+  :class="(() => { return 'hello' })()"
+  :key="index /* hello */ "
+  :key="index // hello "
+  @click="() => {console.log(test)}"
+  @click="
+    () => {
+      console.log(test);
+    }
+  "
+  @click="doSomething()"
+  @click="doSomething;"
+></div>
+</template>
+
+=====================================output=====================================
+<template>
+  <div
+    v-for="item in items"
+    v-for="item of items"
+    v-for="(item, index) in items"
+    v-for="value in object"
+    v-for="(value, key) in object"
+    v-for="(value, key) of object"
+    v-for="(value, key, index) in object"
+    v-for="n in evenNumbers"
+    v-for="n in even(numbers)"
+    v-for="n in 10"
+    v-for="{ a } in [0].map(() => ({ a: 1 }))"
+    v-for="({ a }, [c]) in [0].map(() => 1)"
+    v-for="n in items.map(x => {
+      return x
+    })"
+    @click="/* hello */"
+    @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */ /* 5 */"
+    @click="$emit('click')"
+    @click="$emit('click')"
+    @click="
+      $emit('click')
+      if (something) {
+        for (let i = j; i < 100; i++) {}
+      } else {
+      }
+    "
+    slot-scope="{
+      destructuring: {
+        a: { b }
+      }
+    }"
+    :class="{
+      longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true
+    }"
+    :class="
+      (() => {
+        return 'hello'
+      })()
+    "
+    :key="index /* hello */"
+    :key="
+      index // hello
+    "
+    @click="
+      () => {
+        console.log(test)
+      }
+    "
+    @click="
+      () => {
+        console.log(test)
+      }
+    "
+    @click="doSomething()"
+    @click="doSomething;"
   ></div>
 </template>
 
@@ -355,7 +470,7 @@ export default {
     :data-issue-id="issue.id"
     @mousedown="mouseDown"
     @mousemove="mouseMove"
-    @mouseup="showIssue($event);"
+    @mouseup="showIssue($event)"
   >
     <issue-card-inner
       :list="list"
@@ -521,7 +636,173 @@ export default {
     :data-issue-id="issue.id"
     @mousedown="mouseDown"
     @mousemove="mouseMove"
-    @mouseup="showIssue($event);"
+    @mouseup="showIssue($event)"
+  >
+    <issue-card-inner
+      :list="list"
+      :issue="issue"
+      :issue-link-base="issueLinkBase"
+      :root-path="rootPath"
+      :update-filters="true"
+    />
+  </li>
+</template>
+
+================================================================================
+`;
+
+exports[`board_card.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<script>
+import './issue_card_inner';
+import eventHub from '../eventhub';
+
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+  name: 'BoardsIssueCard',
+  components: {
+    'issue-card-inner': gl.issueBoards.IssueCardInner,
+  },
+  props: {
+    list: Object,
+    issue: Object,
+    issueLinkBase: String,
+    disabled: Boolean,
+    index: Number,
+    rootPath: String,
+  },
+  data() {
+    return {
+      showDetail: false,
+      detailIssue: Store.detail,
+    };
+  },
+  computed: {
+    issueDetailVisible() {
+      return (
+        this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id
+      );
+    },
+  },
+  methods: {
+    mouseDown() {
+      this.showDetail = true;
+    },
+    mouseMove() {
+      this.showDetail = false;
+    },
+    showIssue(e) {
+      if (e.target.classList.contains('js-no-trigger')) return;
+
+      if (this.showDetail) {
+        this.showDetail = false;
+
+        if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+          eventHub.$emit('clearDetailIssue');
+        } else {
+          eventHub.$emit('newDetailIssue', this.issue);
+          Store.detail.list = this.list;
+        }
+      }
+    },
+  },
+};
+</script>
+
+<template>
+  <li class="card"
+    :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+    :index="index"
+    :data-issue-id="issue.id"
+    @mousedown="mouseDown"
+    @mousemove="mouseMove"
+    @mouseup="showIssue($event)">
+    <issue-card-inner
+      :list="list"
+      :issue="issue"
+      :issue-link-base="issueLinkBase"
+      :root-path="rootPath"
+      :update-filters="true" />
+  </li>
+</template>
+
+=====================================output=====================================
+<script>
+import "./issue_card_inner"
+import eventHub from "../eventhub"
+
+const Store = gl.issueBoards.BoardsStore
+
+export default {
+  name: "BoardsIssueCard",
+  components: {
+    "issue-card-inner": gl.issueBoards.IssueCardInner
+  },
+  props: {
+    list: Object,
+    issue: Object,
+    issueLinkBase: String,
+    disabled: Boolean,
+    index: Number,
+    rootPath: String
+  },
+  data() {
+    return {
+      showDetail: false,
+      detailIssue: Store.detail
+    }
+  },
+  computed: {
+    issueDetailVisible() {
+      return (
+        this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id
+      )
+    }
+  },
+  methods: {
+    mouseDown() {
+      this.showDetail = true
+    },
+    mouseMove() {
+      this.showDetail = false
+    },
+    showIssue(e) {
+      if (e.target.classList.contains("js-no-trigger")) return
+
+      if (this.showDetail) {
+        this.showDetail = false
+
+        if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+          eventHub.$emit("clearDetailIssue")
+        } else {
+          eventHub.$emit("newDetailIssue", this.issue)
+          Store.detail.list = this.list
+        }
+      }
+    }
+  }
+}
+</script>
+
+<template>
+  <li
+    class="card"
+    :class="{
+      'user-can-drag': !disabled && issue.id,
+      'is-disabled': disabled || !issue.id,
+      'is-active': issueDetailVisible
+    }"
+    :index="index"
+    :data-issue-id="issue.id"
+    @mousedown="mouseDown"
+    @mousemove="mouseMove"
+    @mouseup="showIssue($event)"
   >
     <issue-card-inner
       :list="list"
@@ -595,21 +876,51 @@ trailingComma: "es5"
 ================================================================================
 `;
 
-exports[`filter.vue 1`] = `
+exports[`custom-block.vue 3`] = `
 ====================================options=====================================
 parsers: ["vue"]
 printWidth: 80
+semi: false
                                                                                 | printWidth
 =====================================input======================================
-<!-- vue filters are only allowed in v-bind and interpolation -->
-<template>
-  <div class="allowed">{{value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird}}</div>
-  <div class="allowed" v-bind:something='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
-  <div class="allowed" :class='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
-  <div class="not-allowed" v-if='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
-</template>
-
-=====================================output=====================================
+<i18n>
+  en:
+    one: One
+    two: Two
+</i18n>
+
+<html><head></head><body></body></html>
+
+=====================================output=====================================
+<i18n>
+  en:
+    one: One
+    two: Two
+</i18n>
+
+<html>
+  <head></head>
+  <body></body>
+</html>
+
+================================================================================
+`;
+
+exports[`filter.vue 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<!-- vue filters are only allowed in v-bind and interpolation -->
+<template>
+  <div class="allowed">{{value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird}}</div>
+  <div class="allowed" v-bind:something='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
+  <div class="allowed" :class='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
+  <div class="not-allowed" v-if='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
+</template>
+
+=====================================output=====================================
 <!-- vue filters are only allowed in v-bind and interpolation -->
 <template>
   <div class="allowed">
@@ -710,6 +1021,64 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`filter.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<!-- vue filters are only allowed in v-bind and interpolation -->
+<template>
+  <div class="allowed">{{value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird}}</div>
+  <div class="allowed" v-bind:something='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
+  <div class="allowed" :class='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
+  <div class="not-allowed" v-if='value | thisIsARealSuperLongFilterPipe("arg1", arg2) | anotherPipeLongJustForFun | pipeTheThird'></div>
+</template>
+
+=====================================output=====================================
+<!-- vue filters are only allowed in v-bind and interpolation -->
+<template>
+  <div class="allowed">
+    {{
+      value
+        | thisIsARealSuperLongFilterPipe("arg1", arg2)
+        | anotherPipeLongJustForFun
+        | pipeTheThird
+    }}
+  </div>
+  <div
+    class="allowed"
+    v-bind:something="
+      value
+        | thisIsARealSuperLongFilterPipe('arg1', arg2)
+        | anotherPipeLongJustForFun
+        | pipeTheThird
+    "
+  ></div>
+  <div
+    class="allowed"
+    :class="
+      value
+        | thisIsARealSuperLongFilterPipe('arg1', arg2)
+        | anotherPipeLongJustForFun
+        | pipeTheThird
+    "
+  ></div>
+  <div
+    class="not-allowed"
+    v-if="
+      value |
+        thisIsARealSuperLongFilterPipe('arg1', arg2) |
+        anotherPipeLongJustForFun |
+        pipeTheThird
+    "
+  ></div>
+</template>
+
+================================================================================
+`;
+
 exports[`interpolations.vue 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
@@ -971,10 +1340,247 @@ x => {
 ================================================================================
 `;
 
-exports[`pre-child.vue 1`] = `
+exports[`interpolations.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<template>
+<div>Fuga magnam facilis. Voluptatem quaerat porro.{{
+
+
+x => {
+    const hello = 'world'
+    return hello;
+}
+
+
+
+}} Magni consectetur in et molestias neque esse voluptatibus voluptas. {{  
+    
+    
+    some_variable 
+
+
+
+}} Eum quia nihil nulla esse. Dolorem asperiores vero est error {{
+
+                    preserve
+
+                    invalid
+                    
+                    interpolation
+
+}} reprehenderit voluptates minus {{console.log(  short_interpolation )}} nemo.</div>
+
+<script type="text/jsx">
+  export default {
+    render (h) {
+      return (
+        <ul
+          class={{
+            'a': b,
+            'c': d,
+            "e": f
+          }}
+        >
+          { this.xyz }
+        </ul>
+    )
+  };
+</script>
+
+<div>
+  1234567890123456789012345678901234567890123456789012345678901234567890{{ something }}1234567890
+</div>
+<div>
+  1234567890123456789012345678901234567890123456789012345678901234567890 {{ something }}1234567890
+</div>
+<div>
+  1234567890123456789012345678901234567890123456789012345678901234567890{{ something }} 1234567890
+</div>
+<div>
+  1234567890123456789012345678901234567890123456789012345678901234567890 {{ something }} 1234567890
+</div>
+</template>
+
+=====================================output=====================================
+<template>
+  <div>
+    Fuga magnam facilis. Voluptatem quaerat porro.{{
+      x => {
+        const hello = "world"
+        return hello
+      }
+    }}
+    Magni consectetur in et molestias neque esse voluptatibus voluptas.
+    {{ some_variable }} Eum quia nihil nulla esse. Dolorem asperiores vero est
+    error
+    {{
+
+                    preserve
+
+                    invalid
+                    
+                    interpolation
+
+    }}
+    reprehenderit voluptates minus {{ console.log(short_interpolation) }} nemo.
+  </div>
+
+  <script type="text/jsx">
+    export default {
+      render (h) {
+        return (
+          <ul
+            class={{
+              'a': b,
+              'c': d,
+              "e": f
+            }}
+          >
+            { this.xyz }
+          </ul>
+      )
+    };
+  </script>
+
+  <div>
+    1234567890123456789012345678901234567890123456789012345678901234567890{{
+      something
+    }}1234567890
+  </div>
+  <div>
+    1234567890123456789012345678901234567890123456789012345678901234567890
+    {{ something }}1234567890
+  </div>
+  <div>
+    1234567890123456789012345678901234567890123456789012345678901234567890{{
+      something
+    }}
+    1234567890
+  </div>
+  <div>
+    1234567890123456789012345678901234567890123456789012345678901234567890
+    {{ something }} 1234567890
+  </div>
+</template>
+
+================================================================================
+`;
+
+exports[`pre-child.vue 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<template>
+<!-- copied from https://github.com/gitlabhq/gitlabhq/blob/master/app/assets/javascripts/ide/components/jobs/detail.vue -->
+<pre
+  ref="buildTrace"
+  class="build-trace mb-0 h-100"
+  @scroll="scrollBuildLog"
+>
+  <code
+    v-show="!detailJob.isLoading"
+    class="bash"
+    v-html="jobOutput"
+  >
+  </code>
+  <div
+    v-show="detailJob.isLoading"
+    class="build-loader-animation"
+  >
+    <div class="dot"></div>
+    <div class="dot"></div>
+    <div class="dot"></div>
+  </div>
+</pre>
+</template>
+
+<!-- copied from https://github.com/gitlabhq/gitlabhq/blob/master/app/assets/javascripts/vue_shared/components/code_block.vue -->
+<template>
+  <pre class="code-block rounded">
+    <code class="d-block">{{ code }}</code>
+  </pre>
+</template>
+
+<template>
+<pre class='woot'>
+  {{ stuff }}
+  </pre>
+</template>
+
+<template>
+<pre class='woot'>
+  123{{ wqeqwwqwqweqweqwewwq || wqeqwwqwqweqweqwewwq || wqeqwwqwqweqweqwewwq || wqeqwwqwqweqweqwewwq }}123
+  123{{ wqeqwwqwqweqweqwewwq || wqeqwwqwqweqweqwewwq || wqeqwwqwqweqweqwewwq || wqeqwwqwqweqweqwewwq }}123
+    </pre>
+</template>
+
+=====================================output=====================================
+<template>
+  <!-- copied from https://github.com/gitlabhq/gitlabhq/blob/master/app/assets/javascripts/ide/components/jobs/detail.vue -->
+  <pre ref="buildTrace" class="build-trace mb-0 h-100" @scroll="scrollBuildLog">
+  <code
+    v-show="!detailJob.isLoading"
+    class="bash"
+    v-html="jobOutput"
+  >
+  </code>
+  <div
+    v-show="detailJob.isLoading"
+    class="build-loader-animation"
+  >
+    <div class="dot"></div>
+    <div class="dot"></div>
+    <div class="dot"></div>
+  </div>
+</pre>
+</template>
+
+<!-- copied from https://github.com/gitlabhq/gitlabhq/blob/master/app/assets/javascripts/vue_shared/components/code_block.vue -->
+<template>
+  <pre class="code-block rounded">
+    <code class="d-block">{{ code }}</code>
+  </pre>
+</template>
+
+<template>
+  <pre class="woot">
+  {{ stuff }}
+  </pre>
+</template>
+
+<template>
+  <pre class="woot">
+  123{{
+      wqeqwwqwqweqweqwewwq ||
+        wqeqwwqwqweqweqwewwq ||
+        wqeqwwqwqweqweqwewwq ||
+        wqeqwwqwqweqweqwewwq
+    }}123
+  123{{
+      wqeqwwqwqweqweqwewwq ||
+        wqeqwwqwqweqweqwewwq ||
+        wqeqwwqwqweqweqwewwq ||
+        wqeqwwqwqweqweqwewwq
+    }}123
+    </pre
+  >
+</template>
+
+================================================================================
+`;
+
+exports[`pre-child.vue 2`] = `
 ====================================options=====================================
 parsers: ["vue"]
 printWidth: 80
+trailingComma: "es5"
                                                                                 | printWidth
 =====================================input======================================
 <template>
@@ -1076,11 +1682,11 @@ printWidth: 80
 ================================================================================
 `;
 
-exports[`pre-child.vue 2`] = `
+exports[`pre-child.vue 3`] = `
 ====================================options=====================================
 parsers: ["vue"]
 printWidth: 80
-trailingComma: "es5"
+semi: false
                                                                                 | printWidth
 =====================================input======================================
 <template>
@@ -1215,6 +1821,23 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`script_src.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<script src="https://www.gstatic.com/firebasejs/4.10.1/firebase.js"></script>
+<script src="https://www.gstatic.com/firebasejs/4.10.1/firebase-firestore.js"></script>
+
+=====================================output=====================================
+<script src="https://www.gstatic.com/firebasejs/4.10.1/firebase.js"></script>
+<script src="https://www.gstatic.com/firebasejs/4.10.1/firebase-firestore.js"></script>
+
+================================================================================
+`;
+
 exports[`self_closing.vue 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
@@ -1304,6 +1927,51 @@ foo();
 ================================================================================
 `;
 
+exports[`self_closing.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <div />
+</template>
+
+<script>
+foo( )
+</script>
+
+<template>
+<div class="container">
+  <HomeH />
+  <HomeA />
+  <HomeX />
+  <HomeY />
+</div>
+</template>
+
+=====================================output=====================================
+<template>
+  <div />
+</template>
+
+<script>
+foo()
+</script>
+
+<template>
+  <div class="container">
+    <HomeH />
+    <HomeA />
+    <HomeX />
+    <HomeY />
+  </div>
+</template>
+
+================================================================================
+`;
+
 exports[`self_closing_style.vue 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
@@ -1349,6 +2017,29 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`self_closing_style.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <span :class="$style.root"><slot /></span>
+</template>
+
+<style src="./style.css" module />
+
+=====================================output=====================================
+<template>
+  <span :class="$style.root"><slot /></span>
+</template>
+
+<style src="./style.css" module />
+
+================================================================================
+`;
+
 exports[`tag-name.vue 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
@@ -1386,6 +2077,25 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`tag-name.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <Table></Table>
+</template>
+
+=====================================output=====================================
+<template>
+  <Table></Table>
+</template>
+
+================================================================================
+`;
+
 exports[`template.vue 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
@@ -1517,6 +2227,72 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`template.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<!--copied from https://github.com/gitlabhq/gitlabhq/blob/master/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue-->
+<template>
+  <div class="file-container">
+    <div class="file-content image_file">
+      <img
+        ref="contentImg"
+        :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
+        :src="path"
+        :alt="path"
+        @load="onImgLoad"
+        @click="onImgClick"/>
+      <p
+        v-if="renderInfo"
+        class="file-info prepend-top-10">
+        <template v-if="fileSize>0">
+          {{ fileSizeReadable }}
+        </template>
+        <template v-if="fileSize>0 && width && height">
+          |
+        </template>
+        <template v-if="width && height">
+          W: {{ width }} | H: {{ height }}
+        </template>
+      </p>
+    </div>
+  </div>
+</template>
+
+=====================================output=====================================
+<!--copied from https://github.com/gitlabhq/gitlabhq/blob/master/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue-->
+<template>
+  <div class="file-container">
+    <div class="file-content image_file">
+      <img
+        ref="contentImg"
+        :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
+        :src="path"
+        :alt="path"
+        @load="onImgLoad"
+        @click="onImgClick"
+      />
+      <p v-if="renderInfo" class="file-info prepend-top-10">
+        <template v-if="fileSize > 0">
+          {{ fileSizeReadable }}
+        </template>
+        <template v-if="fileSize > 0 && width && height">
+          |
+        </template>
+        <template v-if="width && height">
+          W: {{ width }} | H: {{ height }}
+        </template>
+      </p>
+    </div>
+  </div>
+</template>
+
+================================================================================
+`;
+
 exports[`template-lang.vue 1`] = `
 ====================================options=====================================
 parsers: ["vue"]
@@ -1572,6 +2348,45 @@ trailingComma: "es5"
 
 
    
+   lang='pug'>
+  .test
+    #foo
+  .bla
+</template>
+
+=====================================output=====================================
+<template lang="pug">
+  .test
+    #foo
+  .bla
+</template>
+
+<template lang="pug">
+  .test
+    #foo
+  .bla
+</template>
+
+================================================================================
+`;
+
+exports[`template-lang.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<template lang="pug">
+  .test
+    #foo
+  .bla
+</template>
+
+<template 
+
+
+   
    lang='pug'>
   .test
     #foo
@@ -1650,3 +2465,32 @@ trailingComma: "es5"
 
 ================================================================================
 `;
+
+exports[`test.vue 3`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+<script>
+</script>
+
+<template>
+  <br />
+  <footer>
+    foo
+    <br/>
+  </footer>
+</template>
+
+=====================================output=====================================
+<script></script>
+
+<template>
+  <br />
+  <footer>foo <br /></footer>
+</template>
+
+================================================================================
+`;
diff --git a/tests/html_vue/attributes.vue b/tests/html_vue/attributes.vue
index 09c3faa909c2..17f6b534f002 100644
--- a/tests/html_vue/attributes.vue
+++ b/tests/html_vue/attributes.vue
@@ -29,5 +29,7 @@
       console.log(test);
     }
   "
+  @click="doSomething()"
+  @click="doSomething;"
 ></div>
 </template>
diff --git a/tests/html_vue/jsfmt.spec.js b/tests/html_vue/jsfmt.spec.js
index 89a7dccff386..cb7655fcd824 100644
--- a/tests/html_vue/jsfmt.spec.js
+++ b/tests/html_vue/jsfmt.spec.js
@@ -1,2 +1,3 @@
 run_spec(__dirname, ["vue"]);
 run_spec(__dirname, ["vue"], { trailingComma: "es5" });
+run_spec(__dirname, ["vue"], { semi: false });
diff --git a/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap
index 7f3413807aa2..45ff254a43c7 100644
--- a/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap
@@ -92,7 +92,7 @@ printWidth: 80
   <div v-bind:id="'&quot;' + id"></div>
   <div v-bind:id="rawId | formatId"></div>
   <div v-bind:id="ok ? 'YES' : 'NO'"></div>
-  <button @click="foo(arg, 'string');"></button>
+  <button @click="foo(arg, 'string')"></button>
 </template>
 
 ================================================================================