Repository: vuejs/eslint-plugin-vue
Pull request #2167 (2023-05-13 06:10:37)

Add `vue/prefer-define-options` rule
This PR adds `vue/prefer-define-options` rule that enforce use of `defineOptions` instead of default export.


Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index abc3419a..5e9e91db 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: |
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-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..a09e7301 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'),
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'
+        })
+      }
+    })
+  }
+}


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