Repository: vuejs/eslint-plugin-vue
Pull request #2165 (2023-05-13 04:05:38)

Add `vue/valid-define-options` rule
This PR adds `vue/valid-define-options` rule that checks whether `defineOptions` compiler macro is valid.


Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index abc3419a..d851e020 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -255,6 +255,7 @@ For example:
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
 | [vue/padding-line-between-tags](./padding-line-between-tags.md) | require or disallow newlines between sibling tags in template | :wrench: | :lipstick: |
 | [vue/padding-lines-in-component-definition](./padding-lines-in-component-definition.md) | require or disallow padding lines in component definition | :wrench: | :lipstick: |
+| [vue/prefer-define-options](./prefer-define-options.md) | enforce use of `defineOptions` instead of default export. | :wrench: | :warning: |
 | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
 | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
 | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
@@ -268,6 +269,7 @@ For example:
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
 | [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
 | [vue/v-on-handler-style](./v-on-handler-style.md) | enforce writing style for handlers in `v-on` directives | :wrench: | :hammer: |
+| [vue/valid-define-options](./valid-define-options.md) | enforce valid `defineOptions` compiler macro |  | :warning: |
 
 </rules-table>
 
diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md
index cd6c3dcf..2e7d6750 100644
--- a/docs/rules/no-unsupported-features.md
+++ b/docs/rules/no-unsupported-features.md
@@ -29,6 +29,9 @@ This rule reports unsupported Vue.js syntax on the specified version.
 - `version` ... The `version` option accepts [the valid version range of `node-semver`](https://github.com/npm/node-semver#range-grammar). Set the version of Vue.js you are using. This option is required.
 - `ignores` ... You can use this `ignores` option to ignore the given features.
 The `"ignores"` option accepts an array of the following strings.
+  - Vue.js 3.3.0+
+    - `"define-slots"` ... `defineSlots()` macro.
+    - `"define-options"` ... `defineOptions()` macro.
   - Vue.js 3.2.0+
     - `"v-memo"` ... [v-memo](https://vuejs.org/api/built-in-directives.html#v-memo) directive.
     - `"v-bind-prop-modifier-shorthand"` ... `v-bind` with `.prop` modifier shorthand.
@@ -100,6 +103,8 @@ The `"ignores"` option accepts an array of the following strings.
 
 ## :books: Further Reading
 
+- [API - defineOptions()](https://vuejs.org/api/sfc-script-setup.html#defineoptions)
+- [API - defineSlots()](https://vuejs.org/api/sfc-script-setup.html#defineslots)
 - [API - v-memo](https://vuejs.org/api/built-in-directives.html#v-memo)
 - [API - v-is](https://v3.vuejs.org/api/directives.html#v-is)
 - [API - v-is (Old)](https://github.com/vuejs/docs-next/blob/008613756c3d781128d96b64a2d27f7598f8f548/src/api/directives.md#v-is)
diff --git a/docs/rules/prefer-define-options.md b/docs/rules/prefer-define-options.md
new file mode 100644
index 00000000..5cb75d2a
--- /dev/null
+++ b/docs/rules/prefer-define-options.md
@@ -0,0 +1,56 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/prefer-define-options
+description: enforce use of `defineOptions` instead of default export.
+---
+# vue/prefer-define-options
+
+> enforce use of `defineOptions` instead of default export.
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule aims to enforce use of `defineOptions` instead of default export in `<script setup>`.
+
+The [`defineOptions()`](https://vuejs.org/api/sfc-script-setup.html#defineoptions) macro was officially introduced in Vue 3.3.
+
+<eslint-code-block fix :rules="{'vue/prefer-define-options': ['error']}">
+
+```vue
+<script setup>
+/* ✓ GOOD */
+defineOptions({ name: 'Foo' })
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/prefer-define-options': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default { name: 'Foo' }
+</script>
+<script setup>
+/* ... */
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [API - defineOptions()](https://vuejs.org/api/sfc-script-setup.html#defineoptions)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-define-options.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-define-options.js)
diff --git a/docs/rules/valid-define-emits.md b/docs/rules/valid-define-emits.md
index f7de8454..0f52775f 100644
--- a/docs/rules/valid-define-emits.md
+++ b/docs/rules/valid-define-emits.md
@@ -17,7 +17,7 @@ This rule checks whether `defineEmits` compiler macro is valid.
 
 This rule reports `defineEmits` compiler macros in the following cases:
 
-- `defineEmits` are referencing locally declared variables.
+- `defineEmits` is 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 {}`.
@@ -139,6 +139,7 @@ Nothing.
 ## :couple: Related Rules
 
 - [vue/define-emits-declaration](./define-emits-declaration.md)
+- [vue/valid-define-options](./valid-define-options.md)
 - [vue/valid-define-props](./valid-define-props.md)
 
 ## :rocket: Version
diff --git a/docs/rules/valid-define-options.md b/docs/rules/valid-define-options.md
new file mode 100644
index 00000000..2cba9813
--- /dev/null
+++ b/docs/rules/valid-define-options.md
@@ -0,0 +1,119 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/valid-define-options
+description: enforce valid `defineOptions` compiler macro
+---
+# vue/valid-define-options
+
+> enforce valid `defineOptions` 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 `defineOptions` compiler macro is valid.
+
+## :book: Rule Details
+
+This rule reports `defineOptions` compiler macros in the following cases:
+
+- `defineOptions` is referencing locally declared variables.
+- `defineOptions` has been called multiple times.
+- Options are not defined in `defineOptions`.
+- `defineOptions` has type arguments.
+- `defineOptions` has `props`, `emits`, `expose` or `slots` options.
+
+<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">
+
+```vue
+<script setup>
+/* ✓ GOOD */
+defineOptions({ name: 'foo' })
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">
+
+```vue
+<script>
+const def = { name: 'foo' }
+</script>
+<script setup>
+/* ✓ GOOD */
+defineOptions(def)
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">
+
+```vue
+<script setup>
+/* ✗ BAD */
+const def = { name: 'foo' }
+defineOptions(def)
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">
+
+```vue
+<script setup>
+/* ✗ BAD */
+defineOptions({ name: 'foo' })
+defineOptions({ inheritAttrs: false })
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">
+
+```vue
+<script setup>
+/* ✗ BAD */
+defineOptions()
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">
+
+```vue
+<script setup lang="ts">
+/* ✗ BAD */
+defineOptions<{ name: 'Foo' }>()
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">
+
+```vue
+<script setup>
+/* ✗ BAD */
+defineOptions({ props: { msg: String } })
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [vue/valid-define-emits](./valid-define-emits.md)
+- [vue/valid-define-props](./valid-define-props.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-options.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-options.js)
diff --git a/docs/rules/valid-define-props.md b/docs/rules/valid-define-props.md
index 4345d242..fc0f707f 100644
--- a/docs/rules/valid-define-props.md
+++ b/docs/rules/valid-define-props.md
@@ -17,7 +17,7 @@ This rule checks whether `defineProps` compiler macro is valid.
 
 This rule reports `defineProps` compiler macros in the following cases:
 
-- `defineProps` are referencing locally declared variables.
+- `defineProps` is referencing locally declared variables.
 - `defineProps` has both a literal type and an argument. e.g. `defineProps<{/*props*/}>({/*props*/})`
 - `defineProps` has been called multiple times.
 - Props are defined in both `defineProps` and `export default {}`.
@@ -140,6 +140,7 @@ Nothing.
 
 - [vue/define-props-declaration](./define-props-declaration.md)
 - [vue/valid-define-emits](./valid-define-emits.md)
+- [vue/valid-define-options](./valid-define-options.md)
 
 ## :rocket: Version
 
diff --git a/docs/rules/valid-v-is.md b/docs/rules/valid-v-is.md
index 6eda3d99..0ef75aac 100644
--- a/docs/rules/valid-v-is.md
+++ b/docs/rules/valid-v-is.md
@@ -59,7 +59,7 @@ Nothing.
 
 ## :books: Further Reading
 
-- [API - v-is](https://vuejs.org/api/built-in-directives.html#v-is)
+- [API - v-is (Recent)](https://github.com/vuejs/docs/blob/8b4f11a4e94d01c7f1c91a60ceaa5b89d6b6de9f/src/api/built-in-directives.md#v-is-)
 - [API - v-is (Old)](https://github.com/vuejs/docs-next/blob/008613756c3d781128d96b64a2d27f7598f8f548/src/api/directives.md#v-is)
 
 ## :rocket: Version
diff --git a/lib/index.js b/lib/index.js
index c7b58086..7bd0fa3b 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -166,6 +166,7 @@ module.exports = {
     'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
     'padding-line-between-tags': require('./rules/padding-line-between-tags'),
     'padding-lines-in-component-definition': require('./rules/padding-lines-in-component-definition'),
+    'prefer-define-options': require('./rules/prefer-define-options'),
     'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
     'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
     'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
@@ -210,6 +211,7 @@ module.exports = {
     'v-slot-style': require('./rules/v-slot-style'),
     'valid-attribute-name': require('./rules/valid-attribute-name'),
     'valid-define-emits': require('./rules/valid-define-emits'),
+    'valid-define-options': require('./rules/valid-define-options'),
     'valid-define-props': require('./rules/valid-define-props'),
     'valid-model-definition': require('./rules/valid-model-definition'),
     'valid-next-tick': require('./rules/valid-next-tick'),
diff --git a/lib/rules/component-definition-name-casing.js b/lib/rules/component-definition-name-casing.js
index e227d4b9..c3a5cbc3 100644
--- a/lib/rules/component-definition-name-casing.js
+++ b/lib/rules/component-definition-name-casing.js
@@ -77,8 +77,7 @@ module.exports = {
       }
     }
 
-    return Object.assign(
-      {},
+    return utils.compositingVisitors(
       utils.executeOnCallVueComponent(context, (node) => {
         if (node.arguments.length === 2) {
           const argument = node.arguments[0]
@@ -94,6 +93,17 @@ module.exports = {
         if (!node) return
         if (!canConvert(node.value)) return
         convertName(node.value)
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefineOptionsEnter(node) {
+          if (node.arguments.length === 0) return
+          const define = node.arguments[0]
+          if (define.type !== 'ObjectExpression') return
+          const nameNode = utils.findProperty(define, 'name')
+          if (!nameNode) return
+          if (!canConvert(nameNode.value)) return
+          convertName(nameNode.value)
+        }
       })
     )
   }
diff --git a/lib/rules/match-component-file-name.js b/lib/rules/match-component-file-name.js
index 70c34009..f30a6b50 100644
--- a/lib/rules/match-component-file-name.js
+++ b/lib/rules/match-component-file-name.js
@@ -119,8 +119,7 @@ module.exports = {
       }
     }
 
-    return Object.assign(
-      {},
+    return utils.compositingVisitors(
       utils.executeOnCallVueComponent(context, (node) => {
         if (node.arguments.length === 2) {
           const argument = node.arguments[0]
@@ -139,6 +138,18 @@ module.exports = {
         if (!canVerify(node.value)) return
         verifyName(node.value)
       }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefineOptionsEnter(node) {
+          componentCount++
+          if (node.arguments.length === 0) return
+          const define = node.arguments[0]
+          if (define.type !== 'ObjectExpression') return
+          const nameNode = utils.findProperty(define, 'name')
+          if (!nameNode) return
+          if (!canVerify(nameNode.value)) return
+          verifyName(nameNode.value)
+        }
+      }),
       {
         'Program:exit'() {
           if (componentCount > 1) return
diff --git a/lib/rules/no-restricted-component-options.js b/lib/rules/no-restricted-component-options.js
index f50f091d..5af68053 100644
--- a/lib/rules/no-restricted-component-options.js
+++ b/lib/rules/no-restricted-component-options.js
@@ -173,13 +173,25 @@ module.exports = {
     /** @type {ParsedOption[]} */
     const options = context.options.map(parseOption)
 
-    return utils.defineVueVisitor(context, {
-      onVueObjectEnter(node) {
-        for (const option of options) {
-          verify(node, option.test, option.message)
+    return utils.compositingVisitors(
+      utils.defineVueVisitor(context, {
+        onVueObjectEnter(node) {
+          for (const option of options) {
+            verify(node, option.test, option.message)
+          }
         }
-      }
-    })
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefineOptionsEnter(node) {
+          if (node.arguments.length === 0) return
+          const define = node.arguments[0]
+          if (define.type !== 'ObjectExpression') return
+          for (const option of options) {
+            verify(define, option.test, option.message)
+          }
+        }
+      })
+    )
 
     /**
      * @param {ObjectExpression} node
diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js
index 803a62ca..0bcd61a5 100644
--- a/lib/rules/no-unsupported-features.js
+++ b/lib/rules/no-unsupported-features.js
@@ -32,7 +32,10 @@ const FEATURES = {
   // Vue.js 3.2.0+
   'v-memo': require('./syntaxes/v-memo'),
   'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'),
-  'v-bind-attr-modifier': require('./syntaxes/v-bind-attr-modifier')
+  'v-bind-attr-modifier': require('./syntaxes/v-bind-attr-modifier'),
+  // Vue.js 3.3.0+
+  'define-options': require('./syntaxes/define-options'),
+  'define-slots': require('./syntaxes/define-slots')
 }
 
 const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
@@ -115,7 +118,12 @@ module.exports = {
       forbiddenVBindPropModifierShorthand:
         '`.prop` shorthand are not supported until Vue.js "3.2.0".',
       forbiddenVBindAttrModifier:
-        '`.attr` modifiers on `v-bind` are not supported until Vue.js "3.2.0".'
+        '`.attr` modifiers on `v-bind` are not supported until Vue.js "3.2.0".',
+      // Vue.js 3.3.0+
+      forbiddenDefineOptions:
+        '`defineOptions()` macros are not supported until Vue.js "3.3.0".',
+      forbiddenDefineSlots:
+        '`defineSlots()` macros are not supported until Vue.js "3.3.0".'
     }
   },
   /** @param {RuleContext} context */
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index cf9dba9d..50e27062 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -334,8 +334,18 @@ module.exports = {
       }
     }
 
-    return utils.executeOnVue(context, (obj) => {
-      checkOrder(obj.properties)
-    })
+    return utils.compositingVisitors(
+      utils.executeOnVue(context, (obj) => {
+        checkOrder(obj.properties)
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefineOptionsEnter(node) {
+          if (node.arguments.length === 0) return
+          const define = node.arguments[0]
+          if (define.type !== 'ObjectExpression') return
+          checkOrder(define.properties)
+        }
+      })
+    )
   }
 }
diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js
index 3f63050b..b2f1a54d 100644
--- a/lib/rules/padding-lines-in-component-definition.js
+++ b/lib/rules/padding-lines-in-component-definition.js
@@ -352,6 +352,16 @@ module.exports = {
             parseOption(emitsOption, OptionKeys.BetweenItems),
             parseOption(emitsOption, OptionKeys.WithinEach)
           )
+        },
+        onDefineOptionsEnter(node) {
+          if (node.arguments.length === 0) return
+          const define = node.arguments[0]
+          if (define.type !== 'ObjectExpression') return
+          verify(
+            define.properties,
+            parseOption(options, OptionKeys.BetweenOptions),
+            parseOption(options, OptionKeys.WithinOption)
+          )
         }
       })
     )
diff --git a/lib/rules/prefer-define-options.js b/lib/rules/prefer-define-options.js
new file mode 100644
index 00000000..cd924044
--- /dev/null
+++ b/lib/rules/prefer-define-options.js
@@ -0,0 +1,122 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce use of `defineOptions` instead of default export.',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/prefer-define-options.html'
+    },
+    fixable: 'code',
+    schema: [],
+    messages: {
+      preferDefineOptions: 'Use `defineOptions` instead of default export.'
+    }
+  },
+  /**
+   * @param {RuleContext} context
+   * @returns {RuleListener}
+   */
+  create(context) {
+    const scriptSetup = utils.getScriptSetupElement(context)
+    if (!scriptSetup) {
+      return {}
+    }
+
+    /** @type {CallExpression | null} */
+    let defineOptionsNode = null
+    /** @type {ExportDefaultDeclaration | null} */
+    let exportDefaultDeclaration = null
+
+    return utils.compositingVisitors(
+      utils.defineScriptSetupVisitor(context, {
+        onDefineOptionsEnter(node) {
+          defineOptionsNode = node
+        }
+      }),
+      {
+        ExportDefaultDeclaration(node) {
+          exportDefaultDeclaration = node
+        },
+        'Program:exit'() {
+          if (!exportDefaultDeclaration) {
+            return
+          }
+          context.report({
+            node: exportDefaultDeclaration,
+            messageId: 'preferDefineOptions',
+            fix: defineOptionsNode
+              ? null
+              : buildFix(exportDefaultDeclaration, scriptSetup)
+          })
+        }
+      }
+    )
+
+    /**
+     * @param {ExportDefaultDeclaration} node
+     * @param {VElement} scriptSetup
+     * @returns {(fixer: RuleFixer) => Fix[]}
+     */
+    function buildFix(node, scriptSetup) {
+      return (fixer) => {
+        const sourceCode = context.getSourceCode()
+
+        // Calc remove range
+        /** @type {Range} */
+        let removeRange = [...node.range]
+
+        const script = scriptSetup.parent.children
+          .filter(utils.isVElement)
+          .find(
+            (node) =>
+              node.name === 'script' && !utils.hasAttribute(node, 'setup')
+          )
+        if (
+          script &&
+          script.endTag &&
+          sourceCode
+            .getTokensBetween(script.startTag, script.endTag, {
+              includeComments: true
+            })
+            .every(
+              (token) =>
+                removeRange[0] <= token.range[0] &&
+                token.range[1] <= removeRange[1]
+            )
+        ) {
+          removeRange = [...script.range]
+        }
+        const removeStartLoc = sourceCode.getLocFromIndex(removeRange[0])
+        if (
+          sourceCode.lines[removeStartLoc.line - 1]
+            .slice(0, removeStartLoc.column)
+            .trim() === ''
+        ) {
+          removeRange[0] =
+            removeStartLoc.line === 1
+              ? 0
+              : sourceCode.getIndexFromLoc({
+                  line: removeStartLoc.line - 1,
+                  column: sourceCode.lines[removeStartLoc.line - 2].length
+                })
+        }
+
+        return [
+          fixer.removeRange(removeRange),
+          fixer.insertTextAfter(
+            scriptSetup.startTag,
+            `\ndefineOptions(${sourceCode.getText(node.declaration)})\n`
+          )
+        ]
+      }
+    }
+  }
+}
diff --git a/lib/rules/syntaxes/define-options.js b/lib/rules/syntaxes/define-options.js
new file mode 100644
index 00000000..0ef008ca
--- /dev/null
+++ b/lib/rules/syntaxes/define-options.js
@@ -0,0 +1,74 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../../utils/index')
+
+module.exports = {
+  supported: '>=3.3.0',
+  /** @param {RuleContext} context @returns {RuleListener} */
+  createScriptVisitor(context) {
+    const sourceCode = context.getSourceCode()
+    return utils.defineScriptSetupVisitor(context, {
+      onDefineOptionsEnter(node) {
+        context.report({
+          node,
+          messageId: 'forbiddenDefineOptions',
+          fix(fixer) {
+            return fix(fixer, node)
+          }
+        })
+      }
+    })
+
+    /**
+     * @param {RuleFixer} fixer
+     * @param {CallExpression} node defineOptions() node
+     */
+    function fix(fixer, node) {
+      if (node.arguments.length === 0) return null
+      const scriptSetup = utils.getScriptSetupElement(context)
+      if (!scriptSetup) return null
+      if (
+        scriptSetup.parent.children
+          .filter(utils.isVElement)
+          .some(
+            (node) =>
+              node.name === 'script' && !utils.hasAttribute(node, 'setup')
+          )
+      ) {
+        // has `<script>`
+        return null
+      }
+
+      // Find defineOptions statement
+      /** @type {ASTNode} */
+      let statement = node
+      while (statement.parent && statement.parent.type !== 'Program') {
+        statement = statement.parent
+      }
+      // Calc remove range
+      /** @type {Range} */
+      const removeRange = [...statement.range]
+      if (
+        sourceCode.lines[statement.loc.start.line - 1]
+          .slice(0, statement.loc.start.column)
+          .trim() === ''
+      ) {
+        removeRange[0] -= statement.loc.start.column
+      }
+
+      return [
+        fixer.insertTextBefore(
+          scriptSetup,
+          `<script>\nexport default ${sourceCode.getText(
+            node.arguments[0]
+          )}\n</script>\n`
+        ),
+        fixer.removeRange(removeRange)
+      ]
+    }
+  }
+}
diff --git a/lib/rules/syntaxes/define-slots.js b/lib/rules/syntaxes/define-slots.js
new file mode 100644
index 00000000..450ec3e8
--- /dev/null
+++ b/lib/rules/syntaxes/define-slots.js
@@ -0,0 +1,22 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../../utils/index')
+
+module.exports = {
+  supported: '>=3.3.0',
+  /** @param {RuleContext} context @returns {RuleListener} */
+  createScriptVisitor(context) {
+    return utils.defineScriptSetupVisitor(context, {
+      onDefineSlotsEnter(node) {
+        context.report({
+          node,
+          messageId: 'forbiddenDefineSlots'
+        })
+      }
+    })
+  }
+}
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
index 239da270..64d3fd15 100644
--- a/lib/rules/valid-define-emits.js
+++ b/lib/rules/valid-define-emits.js
@@ -20,7 +20,7 @@ module.exports = {
     messages: {
       hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
       referencingLocally:
-        '`defineEmits` are referencing locally declared variables.',
+        '`defineEmits` is referencing locally declared variables.',
       multiple: '`defineEmits` has been called multiple times.',
       notDefined: 'Custom events are not defined.',
       definedInBoth:
@@ -85,7 +85,7 @@ module.exports = {
                 if (utils.withinTypeNode(node)) {
                   continue
                 }
-                //`defineEmits` are referencing locally declared variables.
+                //`defineEmits` is referencing locally declared variables.
                 context.report({
                   node,
                   messageId: 'referencingLocally'
diff --git a/lib/rules/valid-define-options.js b/lib/rules/valid-define-options.js
new file mode 100644
index 00000000..784ac3fb
--- /dev/null
+++ b/lib/rules/valid-define-options.js
@@ -0,0 +1,127 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('@eslint-community/eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce valid `defineOptions` compiler macro',
+      // TODO Switch in the next major version
+      // categories: ['vue3-essential', 'essential'],
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/valid-define-options.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      referencingLocally:
+        '`defineOptions` is referencing locally declared variables.',
+      multiple: '`defineOptions` has been called multiple times.',
+      notDefined: 'Options are not defined.',
+      disallowProp:
+        '`defineOptions()` cannot be used to declare `{{propName}}`. Use `{{insteadMacro}}()` instead.',
+      typeArgs: '`defineOptions()` cannot accept type arguments.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const scriptSetup = utils.getScriptSetupElement(context)
+    if (!scriptSetup) {
+      return {}
+    }
+
+    /** @type {Set<Expression | SpreadElement>} */
+    const optionsDefExpressions = new Set()
+    /** @type {CallExpression[]} */
+    const defineOptionsNodes = []
+
+    return utils.compositingVisitors(
+      utils.defineScriptSetupVisitor(context, {
+        onDefineOptionsEnter(node) {
+          defineOptionsNodes.push(node)
+
+          if (node.arguments.length > 0) {
+            const define = node.arguments[0]
+            if (define.type === 'ObjectExpression') {
+              for (const [propName, insteadMacro] of [
+                ['props', 'defineProps'],
+                ['emits', 'defineEmits'],
+                ['expose', 'defineExpose'],
+                ['slots', 'defineSlots']
+              ]) {
+                const prop = utils.findProperty(define, propName)
+                if (prop) {
+                  context.report({
+                    node,
+                    messageId: 'disallowProp',
+                    data: { propName, insteadMacro }
+                  })
+                }
+              }
+            }
+
+            optionsDefExpressions.add(node.arguments[0])
+          } else {
+            context.report({
+              node,
+              messageId: 'notDefined'
+            })
+          }
+
+          if (node.typeParameters) {
+            context.report({
+              node: node.typeParameters,
+              messageId: 'typeArgs'
+            })
+          }
+        },
+        Identifier(node) {
+          for (const defineOptions of optionsDefExpressions) {
+            if (utils.inRange(defineOptions.range, node)) {
+              const variable = findVariable(context.getScope(), node)
+              if (
+                variable &&
+                variable.references.some((ref) => ref.identifier === node) &&
+                variable.defs.length > 0 &&
+                variable.defs.every(
+                  (def) =>
+                    def.type !== 'ImportBinding' &&
+                    utils.inRange(scriptSetup.range, def.name) &&
+                    !utils.inRange(defineOptions.range, def.name)
+                )
+              ) {
+                if (utils.withinTypeNode(node)) {
+                  continue
+                }
+                //`defineOptions` is referencing locally declared variables.
+                context.report({
+                  node,
+                  messageId: 'referencingLocally'
+                })
+              }
+            }
+          }
+        }
+      }),
+      {
+        'Program:exit'() {
+          if (defineOptionsNodes.length > 1) {
+            // `defineOptions` has been called multiple times.
+            for (const node of defineOptionsNodes) {
+              context.report({
+                node,
+                messageId: 'multiple'
+              })
+            }
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/lib/rules/valid-define-props.js b/lib/rules/valid-define-props.js
index fe471dc1..43cce1d3 100644
--- a/lib/rules/valid-define-props.js
+++ b/lib/rules/valid-define-props.js
@@ -21,7 +21,7 @@ module.exports = {
       hasTypeAndArg:
         '`defineProps` has both a type-only props and an argument.',
       referencingLocally:
-        '`defineProps` are referencing locally declared variables.',
+        '`defineProps` is referencing locally declared variables.',
       multiple: '`defineProps` has been called multiple times.',
       notDefined: 'Props are not defined.',
       definedInBoth:
@@ -86,7 +86,7 @@ module.exports = {
                 if (utils.withinTypeNode(node)) {
                   continue
                 }
-                //`defineProps` are referencing locally declared variables.
+                //`defineProps` is referencing locally declared variables.
                 context.report({
                   node,
                   messageId: 'referencingLocally'


Test patch:
diff --git a/tests/lib/rules/component-definition-name-casing.js b/tests/lib/rules/component-definition-name-casing.js
index ef9cbde8..2349f5d3 100644
--- a/tests/lib/rules/component-definition-name-casing.js
+++ b/tests/lib/rules/component-definition-name-casing.js
@@ -145,6 +145,26 @@ ruleTester.run('component-definition-name-casing', rule, {
       filename: 'test.js',
       code: `fn1(component.data)`,
       parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `<script setup> defineOptions({}) </script>`,
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `<script setup> defineOptions({name: 'FooBar'}) </script>`,
+      options: ['PascalCase'],
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `<script setup> defineOptions({name: 'foo-bar'}) </script>`,
+      options: ['kebab-case'],
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions
     }
   ],
 
@@ -393,6 +413,34 @@ ruleTester.run('component-definition-name-casing', rule, {
           line: 1
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<script setup> defineOptions({name: 'foo-bar'}) </script>`,
+      output: `<script setup> defineOptions({name: 'FooBar'}) </script>`,
+      options: ['PascalCase'],
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions,
+      errors: [
+        {
+          message: 'Property name "foo-bar" is not PascalCase.',
+          line: 1
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<script setup> defineOptions({name: 'FooBar'}) </script>`,
+      output: `<script setup> defineOptions({name: 'foo-bar'}) </script>`,
+      options: ['kebab-case'],
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions,
+      errors: [
+        {
+          message: 'Property name "FooBar" is not kebab-case.',
+          line: 1
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/match-component-file-name.js b/tests/lib/rules/match-component-file-name.js
index cbd35713..82c75ccc 100644
--- a/tests/lib/rules/match-component-file-name.js
+++ b/tests/lib/rules/match-component-file-name.js
@@ -539,6 +539,13 @@ ruleTester.run('match-component-file-name', rule, {
       filename: 'test.jsx',
       code: `fn1(component.data)`,
       parserOptions
+    },
+    {
+      filename: 'MyComponent.vue',
+      code: `<script setup> defineOptions({name: 'MyComponent'}) </script>`,
+      options: [{ extensions: ['vue'] }],
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions
     }
   ],
 
@@ -1081,6 +1088,25 @@ ruleTester.run('match-component-file-name', rule, {
           ]
         }
       ]
+    },
+    {
+      filename: 'MyComponent.vue',
+      code: `<script setup> defineOptions({name: 'CoolComponent'}) </script>`,
+      options: [{ extensions: ['vue'] }],
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions,
+      errors: [
+        {
+          message:
+            'Component name `CoolComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `<script setup> defineOptions({name: 'MyComponent'}) </script>`
+            }
+          ]
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-restricted-component-options.js b/tests/lib/rules/no-restricted-component-options.js
index 87ff4e41..484f6700 100644
--- a/tests/lib/rules/no-restricted-component-options.js
+++ b/tests/lib/rules/no-restricted-component-options.js
@@ -101,6 +101,11 @@ tester.run('no-restricted-component-options', rule, {
       </script>
       `,
       options: [['foo', 'bar']]
+    },
+    {
+      filename: 'test.vue',
+      code: `<script setup> defineOptions({ name: 'Foo' }) </script>`,
+      options: ['Foo']
     }
   ],
   invalid: [
@@ -304,6 +309,17 @@ tester.run('no-restricted-component-options', rule, {
           line: 5
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<script setup> defineOptions({ Foo: 'Foo' }) </script>`,
+      options: ['Foo'],
+      errors: [
+        {
+          message: 'Using `Foo` is not allowed.',
+          line: 1
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-unsupported-features/define-options.js b/tests/lib/rules/no-unsupported-features/define-options.js
new file mode 100644
index 00000000..10b2069b
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/define-options.js
@@ -0,0 +1,88 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('define-options', '^3.2.0')
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-unsupported-features/define-options', rule, {
+  valid: [
+    {
+      code: `
+      <script setup>
+        defineOptions({})
+      </script>`,
+      options: buildOptions({ version: '^3.3.0' })
+    },
+    {
+      code: `
+      <script setup>
+        defineProps({})
+      </script>`,
+      options: buildOptions()
+    },
+    {
+      code: `
+      <script setup>
+        defineOptions({})
+      </script>`,
+      options: buildOptions({ version: '^3.0.0', ignores: ['define-options'] })
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <script setup>
+        defineOptions({ name: 'Foo' })
+      </script>`,
+      options: buildOptions(),
+      output: `
+      <script>
+export default { name: 'Foo' }
+</script>
+<script setup>
+
+      </script>`,
+      errors: [
+        {
+          message:
+            '`defineOptions()` macros are not supported until Vue.js "3.3.0".',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <script setup>
+        defineOptions({});
+      </script>`,
+      options: buildOptions(),
+      output: `
+      <script>
+export default {}
+</script>
+<script setup>
+
+      </script>`,
+      errors: [
+        {
+          message:
+            '`defineOptions()` macros are not supported until Vue.js "3.3.0".',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unsupported-features/define-slots.js b/tests/lib/rules/no-unsupported-features/define-slots.js
new file mode 100644
index 00000000..2fef117c
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/define-slots.js
@@ -0,0 +1,59 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('define-slots', '^3.2.0')
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('no-unsupported-features/define-slots', rule, {
+  valid: [
+    {
+      code: `
+      <script setup>
+        const slots = defineSlots()
+      </script>`,
+      options: buildOptions({ version: '^3.3.0' })
+    },
+    {
+      code: `
+      <script setup>
+        defineProps({})
+      </script>`,
+      options: buildOptions()
+    },
+    {
+      code: `
+      <script setup>
+        const slots = defineSlots()
+      </script>`,
+      options: buildOptions({ version: '^3.0.0', ignores: ['define-slots'] })
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <script setup>
+        const slots = defineSlots()
+      </script>`,
+      options: buildOptions(),
+      errors: [
+        {
+          message:
+            '`defineSlots()` macros are not supported until Vue.js "3.3.0".',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index 873df3b0..b12c41a3 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -165,6 +165,19 @@ ruleTester.run('order-in-components', rule, {
         new Vue()
       `,
       parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      filename: 'example.vue',
+      code: `
+      <script setup>
+        defineOptions({
+          name: 'Foo',
+          inheritAttrs: true,
+        })
+      </script>
+      `,
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions
     }
   ],
 
@@ -943,6 +956,34 @@ ruleTester.run('order-in-components', rule, {
           line: 5
         }
       ]
+    },
+    {
+      filename: 'example.vue',
+      code: `
+      <script setup>
+        defineOptions({
+          inheritAttrs: true,
+          name: 'Foo',
+        })
+      </script>
+      `,
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions,
+      output: `
+      <script setup>
+        defineOptions({
+          name: 'Foo',
+          inheritAttrs: true,
+        })
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The "name" property should be above the "inheritAttrs" property on line 4.',
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/padding-lines-in-component-definition.js b/tests/lib/rules/padding-lines-in-component-definition.js
index a4ed4d3c..58647654 100644
--- a/tests/lib/rules/padding-lines-in-component-definition.js
+++ b/tests/lib/rules/padding-lines-in-component-definition.js
@@ -385,6 +385,19 @@ tester.run('padding-lines-in-component-definition', rule, {
         </script>
       `,
       options: ['always']
+    },
+    {
+      filename: 'MyComment.vue',
+      code: `
+        <script setup>
+        defineOptions({
+            name: 'MyComment',
+
+            inheritAttrs: false,
+        })
+        </script>
+      `,
+      options: [{ betweenOptions: 'always', groupSingleLineProperties: false }]
     }
   ],
   invalid: [
@@ -1235,6 +1248,33 @@ tester.run('padding-lines-in-component-definition', rule, {
           line: 16
         }
       ]
+    },
+    {
+      filename: 'MyComment.vue',
+      code: `
+        <script setup>
+        defineOptions({
+            name: 'MyComment',
+            inheritAttrs: false,
+        })
+        </script>
+      `,
+      output: `
+        <script setup>
+        defineOptions({
+            name: 'MyComment',
+
+            inheritAttrs: false,
+        })
+        </script>
+      `,
+      options: [{ betweenOptions: 'always', groupSingleLineProperties: false }],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/prefer-define-options.js b/tests/lib/rules/prefer-define-options.js
new file mode 100644
index 00000000..2fd9bdfe
--- /dev/null
+++ b/tests/lib/rules/prefer-define-options.js
@@ -0,0 +1,109 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/prefer-define-options')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('prefer-define-options', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({ name: 'Foo' })
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default { name: 'Foo' }
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default { name: 'Foo' }
+      </script>
+      <script setup>
+      const props = defineProps(['foo'])
+      </script>
+      `,
+      output: `
+      <script setup>
+defineOptions({ name: 'Foo' })
+
+      const props = defineProps(['foo'])
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Use `defineOptions` instead of default export.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default { name: 'Foo' }
+      </script>
+      <script setup>
+      defineOptions({})
+      </script>
+      `,
+      output: null,
+      errors: [
+        {
+          message: 'Use `defineOptions` instead of default export.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export const A = 42
+      export default { name: 'Foo' }
+      </script>
+      <script setup>
+      const props = defineProps(['foo'])
+      </script>
+      `,
+      output: `
+      <script>
+      export const A = 42
+      </script>
+      <script setup>
+defineOptions({ name: 'Foo' })
+
+      const props = defineProps(['foo'])
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Use `defineOptions` instead of default export.',
+          line: 4
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/valid-define-emits.js b/tests/lib/rules/valid-define-emits.js
index 4dc70ca5..50c9047d 100644
--- a/tests/lib/rules/valid-define-emits.js
+++ b/tests/lib/rules/valid-define-emits.js
@@ -133,7 +133,7 @@ tester.run('valid-define-emits', rule, {
       `,
       errors: [
         {
-          message: '`defineEmits` are referencing locally declared variables.',
+          message: '`defineEmits` is referencing locally declared variables.',
           line: 5
         }
       ]
diff --git a/tests/lib/rules/valid-define-options.js b/tests/lib/rules/valid-define-options.js
new file mode 100644
index 00000000..58d31df6
--- /dev/null
+++ b/tests/lib/rules/valid-define-options.js
@@ -0,0 +1,201 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/valid-define-options')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015, sourceType: 'module' }
+})
+
+tester.run('valid-define-options', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({ name: 'foo' })
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      const def = { name: 'foo' }
+      </script>
+      <script setup>
+      defineOptions(def)
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      code: `
+      <script setup lang="ts">
+      type X = string;
+
+      defineOptions({ name: 'foo' as X })
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      code: `
+      <script setup lang="ts">
+      const str = 'abc'
+
+      defineOptions({ name: 'foo' as (typeof str) })
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import { def } from './defs';
+
+      defineOptions(def);
+      </script>`
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const def = { name: 'Foo' }
+        defineOptions(def)
+      </script>
+      `,
+      errors: [
+        {
+          message: '`defineOptions` is referencing locally declared variables.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        defineOptions({ name: 'Foo' })
+        defineOptions({ name: 'Bar' })
+      </script>
+      `,
+      errors: [
+        {
+          message: '`defineOptions` has been called multiple times.',
+          line: 3
+        },
+        {
+          message: '`defineOptions` has been called multiple times.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        defineOptions()
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Options are not defined.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      defineOptions<{ name: 'Foo' }>()
+      </script>
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
+      errors: [
+        {
+          message: 'Options are not defined.',
+          line: 3
+        },
+        {
+          message: '`defineOptions()` cannot accept type arguments.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        defineOptions({ props: { msg: String } })
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            '`defineOptions()` cannot be used to declare `props`. Use `defineProps()` instead.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        defineOptions({ emits: ['click'] })
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            '`defineOptions()` cannot be used to declare `emits`. Use `defineEmits()` instead.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        defineOptions({ expose: ['foo'] })
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            '`defineOptions()` cannot be used to declare `expose`. Use `defineExpose()` instead.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        defineOptions({ slots: Object })
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            '`defineOptions()` cannot be used to declare `slots`. Use `defineSlots()` instead.',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/valid-define-props.js b/tests/lib/rules/valid-define-props.js
index a4c0d3ca..511e46bb 100644
--- a/tests/lib/rules/valid-define-props.js
+++ b/tests/lib/rules/valid-define-props.js
@@ -136,7 +136,7 @@ tester.run('valid-define-props', rule, {
       `,
       errors: [
         {
-          message: '`defineProps` are referencing locally declared variables.',
+          message: '`defineProps` is referencing locally declared variables.',
           line: 5
         }
       ]