Repository: vuejs/eslint-plugin-vue
Pull request #1530 (2021-07-02 05:39:43)

Update docs related to `<script setup>`.


Patch:
diff --git a/docs/README.md b/docs/README.md
index e1925804..10a5d0d5 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -15,7 +15,7 @@ This plugin allows us to check the `<template>` and `<script>` of `.vue` files w
 ESLint editor integrations are useful to check your code in real-time.
 
 :::warning Status of Vue.js 3.x supports
-This plugin supports the basic syntax of Vue.js 3.0, but the Vue.js 3.0 experimental features `<script setup>` and `<style vars>` are not yet supported. Follow [#1248](https://github.com/vuejs/eslint-plugin-vue/issues/1248) for more details.
+This plugin supports the basic syntax of Vue.js 3.0 and `<script setup>`, but the Vue.js 3.0 experimental feature CSS variable injection is not yet supported. Follow [#1248](https://github.com/vuejs/eslint-plugin-vue/issues/1248) for more details.
 :::
 
 ## :traffic_light: Versioning policy
diff --git a/docs/rules/README.md b/docs/rules/README.md
index df3441d3..3577668f 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -332,6 +332,7 @@ For example:
 | [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
 | [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
 | [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
+| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro |  |
 | [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro |  |
 | [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
 
diff --git a/docs/rules/experimental-script-setup-vars.md b/docs/rules/experimental-script-setup-vars.md
index bf89fa9a..14735e5e 100644
--- a/docs/rules/experimental-script-setup-vars.md
+++ b/docs/rules/experimental-script-setup-vars.md
@@ -19,6 +19,10 @@ This rule will find variables defined in `<script setup="args">` and mark them a
 
 This rule only has an effect when the `no-undef` rule is enabled.
 
+:::warning
+`<script setup="args">` syntax wasn't rejected by Vue's RFC. Check out the [new syntax](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md).
+:::
+
 ## :book: Rule Details
 
 Without this rule this code triggers warning:
diff --git a/docs/rules/valid-define-emits.md b/docs/rules/valid-define-emits.md
new file mode 100644
index 00000000..cf6ea6c1
--- /dev/null
+++ b/docs/rules/valid-define-emits.md
@@ -0,0 +1,133 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/valid-define-emits
+description: enforce valid `defineEmits` compiler macro
+---
+# vue/valid-define-emits
+
+> enforce valid `defineEmits` compiler macro
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+This rule checks whether `defineEmits` compiler macro is valid.
+
+## :book: Rule Details
+
+This rule reports `defineEmits` compiler macros in the following cases:
+
+- `defineEmits` are referencing locally declared variables.
+- `defineEmits` has both a literal type and an argument. e.g. `defineEmits<(e: 'foo')=>void>(['bar'])`
+- `defineEmits` has been called multiple times.
+- Custom events are defined in both `defineEmits` and `export default {}`.
+- Custom events are not defined in either `defineEmits` or `export default {}`.
+
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
+```vue
+<script setup>
+  /* ✓ GOOD */
+  defineEmits({ notify: null })
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
+```vue
+<script setup>
+  /* ✓ GOOD */
+  defineEmits(['notify'])
+</script>
+```
+
+</eslint-code-block>
+
+```vue
+<script setup lang="ts">
+  /* ✓ GOOD */
+  defineEmits<(e: 'notify')=>void>()
+</script>
+```
+
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
+```vue
+<script>
+  const def = { notify: null }
+</script>
+<script setup>
+  /* ✓ GOOD */
+  defineEmits(def)
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
+```vue
+<script setup>
+  /* ✗ BAD */
+  const def = { notify: null }
+  defineEmits(def)
+</script>
+```
+
+</eslint-code-block>
+
+```vue
+<script setup lang="ts">
+  /* ✗ BAD */
+  defineEmits<(e: 'notify')=>void>({ submit: null })
+</script>
+```
+
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
+```vue
+<script setup>
+  /* ✗ BAD */
+  defineEmits({ notify: null })
+  defineEmits({ submit: null })
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
+```vue
+<script>
+  export default {
+    emits: { notify: null }
+  }
+</script>
+<script setup>
+  /* ✗ BAD */
+  defineEmits({ submit: null })
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
+```vue
+<script setup>
+  /* ✗ BAD */
+  defineEmits()
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-emits.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-emits.js)
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 0b3c3d82..f6dc4745 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -72,7 +72,7 @@ By default all rules from **base** and **essential** categories report ESLint er
 :::
 
 :::warning Status of Vue.js 3.x supports
-This plugin supports the basic syntax of Vue.js 3.0, but the Vue.js 3.0 experimental features `<script setup>` and `<style vars>` are not yet supported. Follow [#1248](https://github.com/vuejs/eslint-plugin-vue/issues/1248) for more details.
+This plugin supports the basic syntax of Vue.js 3.0 and `<script setup>`, but the Vue.js 3.0 experimental feature CSS variable injection is not yet supported. Follow [#1248](https://github.com/vuejs/eslint-plugin-vue/issues/1248) for more details.
 :::
 
 ### Running ESLint from the command line
@@ -335,3 +335,84 @@ Note that you cannot use angle-bracket type assertion style (`var x = <foo>bar;`
 You need to turn off Vetur's template validation by adding `vetur.validation.template: false` to your `.vscode/settings.json`.
 
 See also: "[Visual Studio Code](#editor-integrations)" section and [Vetur - Linting](https://vuejs.github.io/vetur/guide/linting-error.html#linting).
+
+### Does not work well with `<script setup>`
+
+#### The variables used in the `<template>` are warned by `no-unused-vars` rule
+
+You must use [vue/script-setup-uses-vars](../rules/script-setup-uses-vars.md) rule.  
+In your configuration, use the rule set provided by `eslint-plugin-vue` or enable it rule.
+
+Example **.eslintrc.js**:
+
+```js
+module.exports = {
+  // Use the rule set.
+  extends: ['plugin:vue/base'],
+  rules: {
+    // Enable vue/script-setup-uses-vars rule
+    'vue/script-setup-uses-vars': 'error',
+  }
+}
+```
+
+#### Compiler macros such as `defineProps` and `defineEmits` are warned by `no-undef` rule
+
+You need to define [global variables](https://eslint.org/docs/user-guide/configuring/language-options#using-configuration-files-1) in your ESLint configuration file.  
+If you don't want to define global variables, use `import { defineProps, defineEmits } from 'vue'`.
+
+Example **.eslintrc.js**:
+
+```js
+module.exports = {
+  globals: {
+    defineProps: "readonly",
+    defineEmits: "readonly",
+    defineExpose: "readonly",
+    withDefaults: "readonly"
+  }
+}
+```
+
+See also [ESLint - Specifying Globals > Using configuration files](https://eslint.org/docs/user-guide/configuring/language-options#using-configuration-files-1).
+
+#### Parsing error with Top Level `await`
+
+##### Using ESLint <= v7.x
+
+The parser `espree` that comes with `ESLint` v7.x doesn't understand the syntax of ES2022, so it can't parse the Top Level `await` either.  
+However, `espree` v8+ can understand the syntax of ES2022 and parse the Top Level `await`.  
+You install `espree` v8+ and specify `"espree"` and ES2022 in your configuration, the parser will be able to parse it.
+
+```js
+module.exports = {
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: 'espree', // <-
+    ecmaVersion: 2022, // <-
+    sourceType: 'module'
+  },
+}
+```
+
+However, note that the AST generated by `espree` v8+ may not work well with some rules of `ESLint` v7.x.
+
+<!--
+##### Using ESLint >= v8.x
+
+You need to specify `2022` for `parserOptions.ecmaVersion`.
+
+```js
+module.exports = {
+  parserOptions: {
+    ecmaVersion: 2022,
+    sourceType: 'module'
+  },
+}
+```
+-->
+
+#### Other Problems
+
+Try searching for existing issues.
+If it does not exist, you should open a new issue and share your repository to reproduce the issue.
diff --git a/lib/index.js b/lib/index.js
index c321450f..8bf6cad4 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -172,6 +172,7 @@ module.exports = {
     'v-on-function-call': require('./rules/v-on-function-call'),
     'v-on-style': require('./rules/v-on-style'),
     'v-slot-style': require('./rules/v-slot-style'),
+    'valid-define-emits': require('./rules/valid-define-emits'),
     'valid-define-props': require('./rules/valid-define-props'),
     'valid-next-tick': require('./rules/valid-next-tick'),
     'valid-template-root': require('./rules/valid-template-root'),
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
new file mode 100644
index 00000000..6b79e83c
--- /dev/null
+++ b/lib/rules/valid-define-emits.js
@@ -0,0 +1,144 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce valid `defineEmits` compiler macro',
+      // TODO Switch in the major version.
+      // categories: ['vue3-essential'],
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/valid-define-emits.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
+      referencingLocally:
+        '`defineEmits` are referencing locally declared variables.',
+      multiple: '`defineEmits` has been called multiple times.',
+      notDefined: 'Custom events are not defined.',
+      definedInBoth:
+        'Custom events are defined in both `defineEmits` and `export default {}`.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const scriptSetup = utils.getScriptSetupElement(context)
+    if (!scriptSetup) {
+      return {}
+    }
+
+    /** @type {Set<Expression | SpreadElement>} */
+    const emitsDefExpressions = new Set()
+    let hasDefaultExport = false
+    /** @type {CallExpression[]} */
+    const defineEmitsNodes = []
+    /** @type {CallExpression | null} */
+    let emptyDefineEmits = null
+
+    return utils.compositingVisitors(
+      utils.defineScriptSetupVisitor(context, {
+        onDefineEmitsEnter(node) {
+          defineEmitsNodes.push(node)
+
+          if (node.arguments.length >= 1) {
+            if (node.typeParameters && node.typeParameters.params.length >= 1) {
+              // `defineEmits` has both a literal type and an argument.
+              context.report({
+                node,
+                messageId: 'hasTypeAndArg'
+              })
+              return
+            }
+
+            emitsDefExpressions.add(node.arguments[0])
+          } else {
+            if (
+              !node.typeParameters ||
+              node.typeParameters.params.length === 0
+            ) {
+              emptyDefineEmits = node
+            }
+          }
+        },
+        Identifier(node) {
+          for (const def of emitsDefExpressions) {
+            if (utils.inRange(def.range, node)) {
+              const variable = findVariable(context.getScope(), node)
+              if (
+                variable &&
+                variable.references.some((ref) => ref.identifier === node)
+              ) {
+                if (
+                  variable.defs.length &&
+                  variable.defs.every((def) =>
+                    utils.inRange(scriptSetup.range, def.name)
+                  )
+                ) {
+                  //`defineEmits` are referencing locally declared variables.
+                  context.report({
+                    node,
+                    messageId: 'referencingLocally'
+                  })
+                }
+              }
+            }
+          }
+        }
+      }),
+      utils.defineVueVisitor(context, {
+        onVueObjectEnter(node, { type }) {
+          if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
+            return
+          }
+
+          hasDefaultExport = Boolean(utils.findProperty(node, 'emits'))
+        }
+      }),
+      {
+        'Program:exit'() {
+          if (!defineEmitsNodes.length) {
+            return
+          }
+          if (defineEmitsNodes.length > 1) {
+            // `defineEmits` has been called multiple times.
+            for (const node of defineEmitsNodes) {
+              context.report({
+                node,
+                messageId: 'multiple'
+              })
+            }
+            return
+          }
+          if (emptyDefineEmits) {
+            if (!hasDefaultExport) {
+              // Custom events are not defined.
+              context.report({
+                node: emptyDefineEmits,
+                messageId: 'notDefined'
+              })
+            }
+          } else {
+            if (hasDefaultExport) {
+              // Custom events are defined in both `defineEmits` and `export default {}`.
+              for (const node of defineEmitsNodes) {
+                context.report({
+                  node,
+                  messageId: 'definedInBoth'
+                })
+              }
+            }
+          }
+        }
+      }
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/valid-define-emits.js b/tests/lib/rules/valid-define-emits.js
new file mode 100644
index 00000000..309375e9
--- /dev/null
+++ b/tests/lib/rules/valid-define-emits.js
@@ -0,0 +1,156 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/valid-define-emits')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015, sourceType: 'module' }
+})
+
+tester.run('valid-define-emits', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        /* ✓ GOOD */
+        defineEmits({ notify: null })
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        /* ✓ GOOD */
+        defineEmits(['notify'])
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+        /* ✓ GOOD */
+        defineEmits<(e: 'notify')=>void>()
+      </script>
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        const def = { notify: null }
+      </script>
+      <script setup>
+        /* ✓ GOOD */
+        defineEmits(def)
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        /* ✗ BAD */
+        const def = { notify: null }
+        defineEmits(def)
+      </script>
+      `,
+      errors: [
+        {
+          message: '`defineEmits` are referencing locally declared variables.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+        /* ✗ BAD */
+        defineEmits<(e: 'notify')=>void>({ submit: null })
+      </script>
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
+      errors: [
+        {
+          message: '`defineEmits` has both a type-only emit and an argument.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        /* ✗ BAD */
+        defineEmits({ notify: null })
+        defineEmits({ submit: null })
+      </script>
+      `,
+      errors: [
+        {
+          message: '`defineEmits` has been called multiple times.',
+          line: 4
+        },
+        {
+          message: '`defineEmits` has been called multiple times.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['notify']
+      }
+      </script>
+      <script setup>
+        /* ✗ BAD */
+        defineEmits({ submit: null })
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'Custom events are defined in both `defineEmits` and `export default {}`.',
+          line: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        /* ✗ BAD */
+        defineEmits()
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Custom events are not defined.',
+          line: 4
+        }
+      ]
+    }
+  ]
+})