Repository: vuejs/eslint-plugin-vue
Pull request #1129 (2020-05-15 00:12:35)

Add `vue/return-in-emits-validator` rule
This PR adds `vue/return-in-emits-validator` rule.

`vue/return-in-emits-validator` rule enforces that a `return` statement is present in `emits` validators. (The `emits` option is a new in Vue.js 3.0.0+)

[Vue RFCs - 0030-emits-option](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0030-emits-option.md)

refs #1035 


Patch:
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/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/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/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
+          }
+        }
+      ),
+    )
+  }
+}


Test patch:
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
+      }]
+    }
+  ]
+})