Repository: vuejs/eslint-plugin-vue
Pull request #1120 (2020-05-10 06:17:33)

Improve HTML comment directives.
- Add the support of descriptions in directive comments.
- Add the support for block-level directive comments.


Patch:
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index a6756baa..1bc13cda 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -63,6 +63,16 @@ if (deprecatedRules.length > 0) {
 }
 
 module.exports = {
+  configureWebpack (_config, _isServer) {
+    return {
+      resolve: {
+        alias: {
+          module: require.resolve('./shim/module')
+        }
+      }
+    }
+  },
+
   base: '/',
   title: 'eslint-plugin-vue',
   description: 'Official ESLint plugin for Vue.js',
diff --git a/docs/.vuepress/shim/module.js b/docs/.vuepress/shim/module.js
new file mode 100644
index 00000000..66ab9785
--- /dev/null
+++ b/docs/.vuepress/shim/module.js
@@ -0,0 +1,3 @@
+module.exports = {
+  createRequire: () => () => null
+}
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 11a34773..d022dd67 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -73,6 +73,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/require-v-for-key](./require-v-for-key.md) | require `v-bind:key` with `v-for` directives |  |
 | [vue/require-valid-default-prop](./require-valid-default-prop.md) | enforce props default values to be valid |  |
 | [vue/return-in-computed-property](./return-in-computed-property.md) | enforce that a return statement is present in computed property |  |
+| [vue/return-in-emits-validator](./return-in-emits-validator.md) | enforce that a return statement is present in emits validator |  |
 | [vue/use-v-on-exact](./use-v-on-exact.md) | enforce usage of `exact` modifier on `v-on` |  |
 | [vue/valid-template-root](./valid-template-root.md) | enforce valid template root |  |
 | [vue/valid-v-bind](./valid-v-bind.md) | enforce valid `v-bind` directives |  |
diff --git a/docs/rules/comment-directive.md b/docs/rules/comment-directive.md
index 7ff91450..33ad1fa9 100644
--- a/docs/rules/comment-directive.md
+++ b/docs/rules/comment-directive.md
@@ -9,7 +9,7 @@ description: support comment-directives in `<template>`
 
 - :gear: This rule is included in all of `"plugin:vue/base"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-essential"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/recommended"` and `"plugin:vue/vue3-recommended"`.
 
-Sole purpose of this rule is to provide `eslint-disable` functionality in `<template>`.
+Sole purpose of this rule is to provide `eslint-disable` functionality in the `<template>` and in the block level.
 It supports usage of the following comments:
 
 - `eslint-disable`
@@ -34,8 +34,55 @@ This rule sends all `eslint-disable`-like comments as errors to the post-process
 ```vue
 <template>
   <!-- eslint-disable-next-line vue/max-attributes-per-line -->
-  <div a="1" b="2" c="3" d="4">
-  </div>
+  <div a="1" b="2" c="3" d="4" />
+</template>
+```
+
+</eslint-code-block>
+
+The `eslint-disable`-like comments can be used in the `<template>` and in the block level.
+
+<eslint-code-block :rules="{'vue/comment-directive': ['error'], 'vue/max-attributes-per-line': ['error'], 'vue/component-tags-order': ['error'] }">
+
+```vue
+<template>
+  <!-- eslint-disable-next-line vue/max-attributes-per-line -->
+  <div a="1" b="2" c="3" d="4" />
+</template>
+
+<!-- eslint-disable-next-line vue/component-tags-order -->
+<script>
+</script>
+```
+
+</eslint-code-block>
+
+The `eslint-disable` comments has no effect after one block.
+
+<eslint-code-block :rules="{'vue/comment-directive': ['error'], 'vue/max-attributes-per-line': ['error'], 'vue/component-tags-order': ['error'] }">
+
+```vue
+<template>
+</template>
+
+<!-- eslint-disable vue/component-tags-order -->
+<style> /* <- Warning has been disabled. */
+</style>
+
+<script> /* <- Warning are not disabled. */
+</script>
+```
+
+</eslint-code-block>
+
+The `eslint-disable`-like comments can include descriptions to explain why the comment is necessary. The description must occur after the directive and is separated from the directive by two or more consecutive `-` characters. For example:
+
+<eslint-code-block :rules="{'vue/comment-directive': ['error'], 'vue/max-attributes-per-line': ['error']}">
+
+```vue
+<template>
+  <!-- eslint-disable-next-line vue/max-attributes-per-line -- Here's a description about why this disabling is necessary. -->
+  <div a="1" b="2" c="3" d="4" />
 </template>
 ```
 
diff --git a/docs/rules/no-template-target-blank.md b/docs/rules/no-template-target-blank.md
index 67dfcc99..2abc719e 100644
--- a/docs/rules/no-template-target-blank.md
+++ b/docs/rules/no-template-target-blank.md
@@ -23,6 +23,8 @@ This rule disallows using `target="_blank"` attribute without `rel="noopener nor
 </temlate>
 ```
 
+</eslint-code-block>
+
 ## :wrench: Options
 
 ```json
@@ -51,6 +53,8 @@ This rule disallows using `target="_blank"` attribute without `rel="noopener nor
 </temlate>
 ```
 
+</eslint-code-block>
+
 ### `{ allowReferrer: true }`
 
 <eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { allowReferrer: true }]}">
@@ -65,6 +69,8 @@ This rule disallows using `target="_blank"` attribute without `rel="noopener nor
 </temlate>
 ```
 
+</eslint-code-block>
+
 ### `{ "enforceDynamicLinks": "always" }` (default)
 
 <eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
@@ -79,6 +85,8 @@ This rule disallows using `target="_blank"` attribute without `rel="noopener nor
 </temlate>
 ```
 
+</eslint-code-block>
+
 ### `{ "enforceDynamicLinks": "never" }`
 
 <eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
@@ -93,6 +101,8 @@ This rule disallows using `target="_blank"` attribute without `rel="noopener nor
 </temlate>
 ```
 
+</eslint-code-block>
+
 ## :mag: Implementation
 
 - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-template-target-blank.js)
diff --git a/docs/rules/return-in-emits-validator.md b/docs/rules/return-in-emits-validator.md
new file mode 100644
index 00000000..1122af37
--- /dev/null
+++ b/docs/rules/return-in-emits-validator.md
@@ -0,0 +1,63 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/return-in-emits-validator
+description: enforce that a return statement is present in emits validator
+---
+# vue/return-in-emits-validator
+> enforce that a return statement is present in emits validator
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+
+## :book: Rule Details
+
+This rule enforces that a `return` statement is present in `emits` validators.
+
+<eslint-code-block :rules="{'vue/return-in-emits-validator': ['error']}">
+
+```vue
+<script>
+export default {
+  emits: {
+    /* ✓ GOOD */
+    foo (evt) {
+      if (evt) {
+        return true
+      } else {
+        return false
+      }
+    },
+    bar: function () {
+      return true
+    },
+    baz (evt) {
+      if (evt) {
+        return true
+      }
+    },
+    /* ✗ BAD */
+    qux: function () {},
+    quux (evt) {
+      if (!evt) {
+        return false
+      }
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [Vue RFCs - 0030-emits-option](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0030-emits-option.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/return-in-emits-validator.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/return-in-emits-validator.js)
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 39d29477..fc8e598e 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -114,7 +114,7 @@ Vue.component('AsyncComponent', (resolve, reject) => {
 
 ### Disabling rules via `<!-- eslint-disable -->`
 
-You can use `<!-- eslint-disable -->`-like HTML comments in the `<template>` of `.vue` files to disable a certain rule temporarily.
+You can use `<!-- eslint-disable -->`-like HTML comments in the `<template>` and in the block level of `.vue` files to disable a certain rule temporarily.
 
 For example:
 
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index c76edfdc..651ff096 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -41,6 +41,7 @@ module.exports = {
     'vue/require-v-for-key': 'error',
     'vue/require-valid-default-prop': 'error',
     'vue/return-in-computed-property': 'error',
+    'vue/return-in-emits-validator': 'error',
     'vue/use-v-on-exact': 'error',
     'vue/valid-template-root': 'error',
     'vue/valid-v-bind': 'error',
diff --git a/lib/index.js b/lib/index.js
index c4dbcb87..fa2bc434 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -97,6 +97,7 @@ module.exports = {
     'require-v-for-key': require('./rules/require-v-for-key'),
     'require-valid-default-prop': require('./rules/require-valid-default-prop'),
     'return-in-computed-property': require('./rules/return-in-computed-property'),
+    'return-in-emits-validator': require('./rules/return-in-emits-validator'),
     'script-indent': require('./rules/script-indent'),
     'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
     'sort-keys': require('./rules/sort-keys'),
diff --git a/lib/rules/comment-directive.js b/lib/rules/comment-directive.js
index d5a1eeb8..2058190f 100644
--- a/lib/rules/comment-directive.js
+++ b/lib/rules/comment-directive.js
@@ -12,6 +12,15 @@
 const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+(\S|\S[\s\S]*\S))?\s*$/
 const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+(\S|\S[\s\S]*\S))?\s*$/
 
+/**
+ * Remove the ignored part from a given directive comment and trim it.
+ * @param {string} value The comment text to strip.
+ * @returns {string} The stripped text.
+ */
+function stripDirectiveComment (value) {
+  return value.split(/\s-{2,}\s/u)[0].trim()
+}
+
 /**
  * Parse a given comment.
  * @param {RegExp} pattern The RegExp pattern to parse.
@@ -19,7 +28,7 @@ const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+(\S|\S[\s\
  * @returns {({type:string,rules:string[]})|null} The parsing result.
  */
 function parse (pattern, comment) {
-  const match = pattern.exec(comment)
+  const match = pattern.exec(stripDirectiveComment(comment))
   if (match == null) {
     return null
   }
@@ -100,6 +109,28 @@ function processLine (context, comment) {
   }
 }
 
+/**
+ * Extracts the top-level elements in document fragment.
+ * @param {VDocumentFragment} documentFragment The document fragment.
+ * @returns {VElement[]} The top-level elements
+ */
+function extractTopLevelHTMLElements (documentFragment) {
+  return documentFragment.children.filter(e => e.type === 'VElement')
+}
+/**
+ * Extracts the top-level comments in document fragment.
+ * @param {VDocumentFragment} documentFragment The document fragment.
+ * @returns {Token[]} The top-level comments
+ */
+function extractTopLevelDocumentFragmentComments (documentFragment) {
+  const elements = extractTopLevelHTMLElements(documentFragment)
+
+  return documentFragment.comments.filter(comment =>
+    elements.every(element =>
+      comment.range[1] <= element.range[0] || element.range[1] <= comment.range[0]
+    ))
+}
+
 // -----------------------------------------------------------------------------
 // Rule Definition
 // -----------------------------------------------------------------------------
@@ -116,24 +147,40 @@ module.exports = {
   },
 
   create (context) {
+    const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
+
     return {
       Program (node) {
-        if (!node.templateBody) {
-          return
-        }
+        if (node.templateBody) {
+          // Send directives to the post-process.
+          for (const comment of node.templateBody.comments) {
+            processBlock(context, comment)
+            processLine(context, comment)
+          }
 
-        // Send directives to the post-process.
-        for (const comment of node.templateBody.comments) {
-          processBlock(context, comment)
-          processLine(context, comment)
+          // Send a clear mark to the post-process.
+          context.report({
+            loc: node.templateBody.loc.end,
+            message: 'clear'
+          })
         }
+        if (documentFragment) {
+          // Send directives to the post-process.
+          for (const comment of extractTopLevelDocumentFragmentComments(documentFragment)) {
+            processBlock(context, comment)
+            processLine(context, comment)
+          }
 
-        // Send a clear mark to the post-process.
-        context.report({
-          loc: node.templateBody.loc.end,
-          message: 'clear'
-        })
+          // Send a clear mark to the post-process.
+          for (const element of extractTopLevelHTMLElements(documentFragment)) {
+            context.report({
+              loc: element.loc.end,
+              message: 'clear'
+            })
+          }
+        }
       }
     }
   }
 }
+
diff --git a/lib/rules/return-in-emits-validator.js b/lib/rules/return-in-emits-validator.js
new file mode 100644
index 00000000..dd5e551f
--- /dev/null
+++ b/lib/rules/return-in-emits-validator.js
@@ -0,0 +1,109 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
+ */
+
+/**
+ * Checks if the given node value is falsy.
+ * @param {Expression} node The node to check
+ * @returns {boolean} If `true`, the given node value is falsy.
+ */
+function isFalsy (node) {
+  if (node.type === 'Literal') {
+    if (node.bigint) {
+      return node.bigint === '0'
+    } else if (!node.value) {
+      return true
+    }
+  } else if (node.type === 'Identifier') {
+    if (node.name === 'undefined' || node.name === 'NaN') {
+      return true
+    }
+  }
+  return false
+}
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce that a return statement is present in emits validator',
+      categories: ['vue3-essential'],
+      url: 'https://eslint.vuejs.org/rules/return-in-emits-validator.html'
+    },
+    fixable: null, // or "code" or "whitespace"
+    schema: []
+  },
+
+  create (context) {
+    const emitsValidators = []
+
+    // ----------------------------------------------------------------------
+    // Public
+    // ----------------------------------------------------------------------
+
+    let scopeStack = null
+
+    return Object.assign({},
+      utils.defineVueVisitor(context,
+        {
+          ObjectExpression (obj, { node: vueNode }) {
+            if (obj !== vueNode) {
+              return
+            }
+            for (const emits of utils.getComponentEmits(obj)) {
+              const emitsValue = emits.value
+              if (!emitsValue) {
+                continue
+              }
+              if (emitsValue.type !== 'FunctionExpression' && emitsValue.type !== 'ArrowFunctionExpression') {
+                continue
+              }
+              emitsValidators.push(emits)
+            }
+          },
+          ':function' (node) {
+            scopeStack = { upper: scopeStack, functionNode: node, hasReturnValue: false, possibleOfReturnTrue: false }
+          },
+          ReturnStatement (node) {
+            if (node.argument) {
+              scopeStack.hasReturnValue = true
+
+              if (!isFalsy(node.argument)) {
+                scopeStack.possibleOfReturnTrue = true
+              }
+            }
+          },
+          ':function:exit' (node) {
+            if (!scopeStack.possibleOfReturnTrue) {
+              const emits = emitsValidators.find(e => e.value === node)
+              if (emits) {
+                context.report({
+                  node,
+                  message: scopeStack.hasReturnValue
+                    ? 'Expected to return a true value in "{{name}}" emits validator.'
+                    : 'Expected to return a boolean value in "{{name}}" emits validator.',
+                  data: {
+                    name: emits.emitName
+                  }
+                })
+              }
+            }
+
+            scopeStack = scopeStack.upper
+          }
+        }
+      ),
+    )
+  }
+}
diff --git a/package.json b/package.json
index 5a703782..2f16f5d3 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "eslint-utils": "^2.0.0",
     "natural-compare": "^1.4.0",
     "semver": "^7.3.2",
-    "vue-eslint-parser": "^7.0.0"
+    "vue-eslint-parser": "^7.1.0"
   },
   "devDependencies": {
     "@types/node": "^13.13.5",


Test patch:
diff --git a/tests/lib/rules/comment-directive.js b/tests/lib/rules/comment-directive.js
index 346c6691..d87e742c 100644
--- a/tests/lib/rules/comment-directive.js
+++ b/tests/lib/rules/comment-directive.js
@@ -226,4 +226,130 @@ describe('comment-directive', () => {
       assert.deepEqual(messages[1].line, 5)
     })
   })
+
+  describe('allow description', () => {
+    it('disable all rules if <!-- eslint-disable -- description -->', () => {
+      const code = `
+        <template>
+          <!-- eslint-disable -- description -->
+          <div id id="a">Hello</div>
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 0)
+    })
+
+    it('enable all rules if <!-- eslint-enable -- description -->', () => {
+      const code = `
+        <template>
+          <!-- eslint-disable -- description -->
+          <div id id="a">Hello</div>
+          <!-- eslint-enable -- description -->
+          <div id id="a">Hello</div>
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 2)
+      assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
+      assert.deepEqual(messages[0].line, 6)
+      assert.deepEqual(messages[1].ruleId, 'vue/no-duplicate-attributes')
+      assert.deepEqual(messages[1].line, 6)
+    })
+
+    it('enable specific rules if <!-- eslint-enable vue/no-duplicate-attributes -- description -->', () => {
+      const code = `
+        <template>
+          <!-- eslint-disable vue/no-parsing-error, vue/no-duplicate-attributes -- description -->
+          <div id id="a">Hello</div>
+          <!-- eslint-enable vue/no-duplicate-attributes -- description -->
+          <div id id="a">Hello</div>
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 1)
+      assert.deepEqual(messages[0].ruleId, 'vue/no-duplicate-attributes')
+      assert.deepEqual(messages[0].line, 6)
+    })
+
+    it('disable all rules if <!-- eslint-disable-line -- description -->', () => {
+      const code = `
+        <template>
+          <div id id="a">Hello</div> <!-- eslint-disable-line -- description -->
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 0)
+    })
+
+    it('disable specific rules if <!-- eslint-disable-line vue/no-duplicate-attributes -- description -->', () => {
+      const code = `
+        <template>
+          <div id id="a">Hello</div> <!-- eslint-disable-line vue/no-duplicate-attributes -- description -->
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 1)
+      assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
+    })
+
+    it('disable all rules if <!-- eslint-disable-next-line -- description -->', () => {
+      const code = `
+        <template>
+          <!-- eslint-disable-next-line -- description -->
+          <div id id="a">Hello</div>
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 0)
+    })
+
+    it('disable specific rules if <!-- eslint-disable-next-line vue/no-duplicate-attributes -->', () => {
+      const code = `
+        <template>
+          <!-- eslint-disable-next-line vue/no-duplicate-attributes -- description -->
+          <div id id="a">Hello</div>
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 1)
+      assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
+    })
+  })
+
+  describe('block level directive', () => {
+    it('disable all rules if <!-- eslint-disable -->', () => {
+      const code = `
+        <!-- eslint-disable -->
+        <template>
+          <div id id="a">Hello</div>
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 0)
+    })
+
+    it('don\'t disable rules if <!-- eslint-disable --> is on after block', () => {
+      const code = `
+        <!-- eslint-disable -->
+        <i18n>
+        </i18n>
+        <template>
+          <div id id="a">Hello</div>
+        </template>
+      `
+      const messages = linter.executeOnText(code, 'test.vue').results[0].messages
+
+      assert.deepEqual(messages.length, 2)
+      assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
+      assert.deepEqual(messages[1].ruleId, 'vue/no-duplicate-attributes')
+    })
+  })
 })
diff --git a/tests/lib/rules/return-in-emits-validator.js b/tests/lib/rules/return-in-emits-validator.js
new file mode 100644
index 00000000..8ee6b75e
--- /dev/null
+++ b/tests/lib/rules/return-in-emits-validator.js
@@ -0,0 +1,275 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/return-in-emits-validator')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+ruleTester.run('return-in-emits-validator', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo () {
+              return true
+            },
+            bar: function (e) {
+              return true
+            },
+            baz: (e) => {
+              return e
+            },
+            qux () {
+              if (foo) {
+                return true
+              } else {
+                return false
+              }
+            },
+            quux: null,
+            corge (evt) {
+              return evt
+            }
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo () {
+              const options = []
+              this.matches.forEach((match) => {
+                options.push(match)
+              })
+              return options
+            }
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: ['foo']
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo () {
+              const options = []
+              this.matches.forEach(function (match) {
+                options.push(match)
+              })
+              return options
+            }
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            a () {
+              return 1n
+            },
+            b: function (e) {
+              return 1
+            },
+            c: (e) => {
+              return 'a'
+            },
+          }
+        }
+        </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo () {
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'Expected to return a boolean value in "foo" emits validator.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo: function () {
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'Expected to return a boolean value in "foo" emits validator.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo: () => {
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'Expected to return a boolean value in "foo" emits validator.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo: function () {
+              function bar () {
+                return this.baz * 2
+              }
+              bar()
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'Expected to return a boolean value in "foo" emits validator.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo () {
+            },
+            bar () {
+              return
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message: 'Expected to return a boolean value in "foo" emits validator.',
+          line: 5
+        },
+        {
+          message: 'Expected to return a boolean value in "bar" emits validator.',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo () {
+              return
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'Expected to return a boolean value in "foo" emits validator.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo: function () {
+              if (a) {
+                return false
+              } else if (b) {
+                return 0
+              } else if (c) {
+                return null
+              } else if (d) {
+                return ''
+              } else if (e) {
+                return undefined
+              } else if (f) {
+                return NaN
+              } else if (g) {
+                return 0n
+              }
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'Expected to return a true value in "foo" emits validator.',
+        line: 5
+      }]
+    }
+  ]
+})