Repository: vuejs/eslint-plugin-vue (js)

Issue:
no-undef-components failed when referencing itself in `<script setup>`
<!--
  ❗Please don't ignore this template.❗

  If you ignore it, we're just going to respond asking you to fill it out, which wastes everyone's time.
  The more relevant information you can include, the faster we can find the issue and fix it without asking you for more info.
-->

<!--
  Before posting the issue, please confirm that the problem you're getting
  is not related with your code editor configuration.
  To make sure it's not, run: yarn eslint src/your-file.vue
-->

**Checklist**

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have read the [FAQ](https://eslint.vuejs.org/user-guide/#faq) and my problem is not listed.
<!-- If you do not read the FAQ and open an issue that is listed in the FAQ, we may silently close the issue. -->

**Tell us about your environment**

- **ESLint version:** 8.28.0
- **eslint-plugin-vue version:** 9.8.0
- **Node version:** 16.17.0
- **Operating System:**  Windows 11

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```js
const path = require('node:path');

module.exports = {
  env: {
    browser: true,
  },

  parser: '@babel/eslint-parser',

  parserOptions: {
    babelOptions: {
      configFile: path.resolve(__dirname, '.babelrc'),
    },
    ecmaVersion: 11,
    sourceType: 'module',
  },

  globals: {
    defineProps: 'readonly',
    defineEmits: 'readonly',
    withDefaults: 'readonly',
    VERSION: 'readonly',
  },

  ignorePatterns: ['dist/**'],

  overrides: [
    {
      files: ['*.ts'],
      extends: [
        'plugin:@typescript-eslint/eslint-recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:@typescript-eslint/recommended-requiring-type-checking',
        'plugin:prettier/recommended',
      ],

      plugins: ['@typescript-eslint'],

      parser: '@typescript-eslint/parser',

      parserOptions: {
        project: path.resolve(__dirname, './tsconfig.eslint.json'),
      },

      rules: {
        '@typescript-eslint/explicit-function-return-type': [
          'warn',
          { allowTypedFunctionExpressions: true },
        ],
        '@typescript-eslint/naming-convention': [
          'warn',
          {
            selector: 'default',
            format: ['camelCase'],
            leadingUnderscore: 'allow',
            trailingUnderscore: 'allow',
          },
          {
            selector: ['variable', 'parameter'],
            format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
            leadingUnderscore: 'allow',
            trailingUnderscore: 'allow',
          },
          {
            selector: 'property',
            format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
            leadingUnderscore: 'allow',
            trailingUnderscore: 'allow',
          },
          {
            selector: 'typeLike',
            format: ['PascalCase'],
          },
        ],

        '@typescript-eslint/no-explicit-any': [
          'warn',
          { ignoreRestArgs: true },
        ],

        '@typescript-eslint/no-non-null-assertion': 'off',

        '@typescript-eslint/no-unsafe-member-access': 'warn',
      },
    },
    {
      files: '*.vue',
      extends: [
        'plugin:vue/vue3-recommended',
        'plugin:@typescript-eslint/eslint-recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:prettier/recommended',
        '@vue/typescript/recommended',
      ],
      plugins: ['@typescript-eslint'],
      parser: 'vue-eslint-parser',
      parserOptions: {
        extraFileExtensions: ['.vue'],
        parser: '@typescript-eslint/parser',
        ecmaVersion: 2020,
      },
      rules: {
        '@typescript-eslint/explicit-function-return-type': [
          'warn',
          { allowTypedFunctionExpressions: true },
        ],

        '@typescript-eslint/naming-convention': [
          'warn',
          {
            selector: 'default',
            format: ['camelCase'],
            leadingUnderscore: 'allow',
            trailingUnderscore: 'allow',
          },
          {
            selector: ['variable', 'parameter'],
            format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
            leadingUnderscore: 'allow',
            trailingUnderscore: 'allow',
          },
          {
            selector: 'property',
            format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
            leadingUnderscore: 'allow',
            trailingUnderscore: 'allow',
          },
          {
            selector: 'typeLike',
            format: ['PascalCase'],
          },
        ],

        '@typescript-eslint/no-explicit-any': [
          'warn',
          { ignoreRestArgs: true },
        ],

        '@typescript-eslint/no-non-null-assertion': 'off',

        'vue/block-lang': ['error', { script: { lang: 'ts' } }],
        'vue/block-tag-newline': 'error',
        'vue/component-api-style': 'error',
        'vue/component-name-in-template-casing': 'error',
        'vue/component-options-name-casing': 'error',
        'vue/custom-event-name-casing': 'error',
        'vue/define-emits-declaration': 'error',
        'vue/define-macros-order': 'error',
        'vue/define-props-declaration': 'error',
        'vue/html-button-has-type': 'error',
        'vue/html-comment-content-newline': 'error',
        'vue/html-comment-content-spacing': 'error',
        'vue/html-comment-indent': 'error',
        'vue/match-component-file-name': 'error',
        'vue/match-component-import-name': 'error',
        'vue/new-line-between-multi-line-property': 'error',
        'vue/next-tick-style': 'error',
        'vue/no-boolean-default': ['warn'],
        'vue/no-duplicate-attr-inheritance': 'error',
        'vue/no-empty-component-block': 'error',
        'vue/no-multiple-objects-in-class': 'error',
        'vue/no-potential-component-option-typo': 'error',
        'vue/no-ref-object-destructure': 'error',
        'vue/no-required-prop-with-default': 'error',
        'vue/no-static-inline-styles': 'error',
        'vue/no-template-target-blank': 'error',
        'vue/no-this-in-before-route-enter': 'error',
        'vue/no-undef-components': 'error',
        'vue/no-undef-properties': ['warn'],
        'vue/no-unsupported-features': 'error',
        'vue/no-unused-properties': 'error',
        'vue/no-unused-refs': 'error',
        'vue/no-useless-mustaches': 'error',
        'vue/no-useless-v-bind': 'error',
        'vue/padding-line-between-blocks': 'error',
        'vue/padding-line-between-tags': 'error',
        'vue/prefer-prop-type-boolean-first': 'error',
        'vue/prefer-separate-static-class': 'error',
        'vue/prefer-true-attribute-shorthand': 'error',
        'vue/require-direct-export': 'error',
        'vue/require-emit-validator': ['warn'],
        'vue/require-expose': 'error',
        'vue/require-name-property': 'error',
        'vue/require-prop-comment': 'error',
        'vue/script-indent': 'error',
        'vue/static-class-names-order': 'error',
        'vue/v-for-delimiter-style': 'error',
        'vue/v-on-handler-style': 'error',
      },
    },
  ],
};

```

**What did you do?**	
<!-- Please include the actual source code causing the issue. -->
```vue
// CommentCard.vue
<template>
  <!-- ... -->
  <CommentCard />
  <!-- ... -->
</template>
<script setup lang="ts">
  // ...
</script>
```

**What did you expect to happen?**

No error 


**What actually happened?**
<!--
  Please include the actual, raw output from ESLint.
  If you are only looking at the results of your editor extension, also check the CLI results.
-->

```
/home/mister-hope/projects/waline/packages/client/src/components/CommentCard.vue
  152:9   error    The '<CommentCard>' component has been used, but not defined                                                               vue/no-undef-components
```

---

I would happy to share more if you do need this, but I do think that it's easy to make a repro, and the test file is not containing the test case where a component using `<script setup>` use itself in template.




Pull request description:
Fix false positives for circular references in <script setup> in `vue/no-undef-components`
fixes #2053
close #2054

Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 2e21edc5..b23f6842 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -253,6 +253,7 @@ For example:
 | [vue/no-v-text](./no-v-text.md) | disallow use of v-text |  | :hammer: |
 | [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-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/padding-lines-in-component-definition.md b/docs/rules/padding-lines-in-component-definition.md
new file mode 100644
index 00000000..ab46a532
--- /dev/null
+++ b/docs/rules/padding-lines-in-component-definition.md
@@ -0,0 +1,163 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/padding-lines-in-component-definition
+description: require or disallow padding lines in component definition
+---
+# vue/padding-lines-in-component-definition
+
+> require or disallow padding lines in component definition
+
+- :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 requires or disallows blank lines in the component definition. Properly blank lines help developers improve code readability and code style flexibility.
+
+<eslint-code-block fix :rules="{'vue/padding-lines-in-component-definition': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default {
+  props: {
+    modelValue: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      count: 0,
+    };
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/padding-lines-in-component-definition': ['error']}">
+
+```vue
+<script>
+/* ✓ GOOD */
+export default {
+  props: {
+    modelValue: {
+      type: String,
+      default: '',
+    },
+  },
+
+  data() {
+    return {
+      count: 0,
+    };
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/padding-lines-in-component-definition": ["error", {
+    "betweenOptions": "always" | "never",
+    
+    "withinOption": { 
+      "props": {
+        "betweenItems": "always" | "never" | "ignore",
+        "withinEach": "always" | "never" | "ignore",
+      } | "always" | "never" | "ignore", // shortcut to set both
+      
+      "data": {
+        "betweenItems": "always" | "never" | "ignore",
+        "withinEach": "always" | "never" | "ignore",
+      } | "always" | "never" | "ignore" // shortcut to set both
+        
+      // ... all options
+    } | "always" | "never" | "ignore",
+    
+    "groupSingleLineProperties": true | false
+  }]
+}
+```
+
+- `betweenOptions` ... Setting padding lines between options. default `always`
+- `withinOption` ... Setting padding lines within option
+  - `emits` ... Setting padding between lines between `emits` and `defineEmits`. default `always`
+  - `props` ... Setting padding between lines between `props` and `defineProps`. default `always`
+  - ...
+- `groupSingleLineProperties` ... Setting groupings of multiple consecutive single-line properties (e.g. `name`, `inheritAttrs`), default `true`
+
+### Group single-line properties
+
+<eslint-code-block fix :rules="{'vue/padding-lines-in-component-definition': ['error', { betweenOptions: 'always', withinOption: 'always', groupSingleLineProperties: true}]}">
+
+```vue
+<script>
+export default {
+  name: 'GroupSingleLineProperties',
+  inheritAttrs: false,
+  
+  props: {
+    modelValue: {
+      type: String,
+      default: '',
+    },
+  },
+  
+  data() {
+    return {
+      count: 0,
+    };
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+### With custom options
+
+<eslint-code-block fix :rules="{'vue/padding-lines-in-component-definition': ['error', { betweenOptions: 'always', withinOption: { props: { betweenItems: 'always', withinEach: 'never' }, customOption: { betweenItems: 'always', withinEach: 'ignore' } }, groupSingleLineProperties: true}]}">
+
+```vue
+<script>
+export default {
+  props: {
+    modelValue: {
+      type: String,
+      default: '',
+    },
+    
+    isActive: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  
+  customOption: {
+    foo: () => {
+      return 'foo'
+    },
+    
+    bar: () => {
+      return 'bar'
+    }
+  },
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-lines-in-component-definition.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-lines-in-component-definition.js)
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index b1486f15..59880b84 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -44,6 +44,7 @@ module.exports = {
     'vue/operator-linebreak': 'off',
     'vue/padding-line-between-blocks': 'off',
     'vue/padding-line-between-tags': 'off',
+    'vue/padding-lines-in-component-definition': 'off',
     'vue/script-indent': 'off',
     'vue/singleline-html-element-content-newline': 'off',
     'vue/space-in-parens': 'off',
diff --git a/lib/index.js b/lib/index.js
index f5da9a70..aef60a60 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -163,6 +163,7 @@ module.exports = {
     'order-in-components': require('./rules/order-in-components'),
     '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-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/no-undef-components.js b/lib/rules/no-undef-components.js
index 4537f3dc..9eb74ad0 100644
--- a/lib/rules/no-undef-components.js
+++ b/lib/rules/no-undef-components.js
@@ -4,6 +4,7 @@
  */
 'use strict'
 
+const path = require('path')
 const utils = require('../utils')
 const casing = require('../utils/casing')
 
@@ -17,6 +18,93 @@ function camelize(str) {
   return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
 }
 
+class DefinedInSetupComponents {
+  constructor() {
+    /**
+     * Component names
+     * @type {Set<string>}
+     */
+    this.names = new Set()
+  }
+
+  /**
+   * @param {string[]} names
+   */
+  addName(...names) {
+    for (const name of names) {
+      this.names.add(name)
+    }
+  }
+
+  /**
+   * @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L334
+   * @param {string} rawName
+   */
+  isDefinedComponent(rawName) {
+    if (this.names.has(rawName)) {
+      return true
+    }
+    const camelName = camelize(rawName)
+    if (this.names.has(camelName)) {
+      return true
+    }
+    const pascalName = casing.capitalize(camelName)
+    if (this.names.has(pascalName)) {
+      return true
+    }
+    // Check namespace
+    // https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L305
+    const dotIndex = rawName.indexOf('.')
+    if (dotIndex > 0 && this.isDefinedComponent(rawName.slice(0, dotIndex))) {
+      return true
+    }
+    return false
+  }
+}
+
+class DefinedInOptionComponents {
+  constructor() {
+    /**
+     * Component names
+     * @type {Set<string>}
+     */
+    this.names = new Set()
+    /**
+     * Component names, transformed to kebab-case
+     * @type {Set<string>}
+     */
+    this.kebabCaseNames = new Set()
+  }
+
+  /**
+   * @param {string[]} names
+   */
+  addName(...names) {
+    for (const name of names) {
+      this.names.add(name)
+      this.kebabCaseNames.add(casing.kebabCase(name))
+    }
+  }
+
+  /**
+   * @param {string} rawName
+   */
+  isDefinedComponent(rawName) {
+    if (this.names.has(rawName)) {
+      return true
+    }
+    const kebabCaseName = casing.kebabCase(rawName)
+    if (
+      this.kebabCaseNames.has(kebabCaseName) &&
+      !casing.isPascalCase(rawName)
+    ) {
+      // Component registered as `foo-bar` cannot be used as `FooBar`
+      return true
+    }
+    return false
+  }
+}
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -109,13 +197,15 @@ module.exports = {
 
     if (utils.isScriptSetup(context)) {
       // For <script setup>
+      const definedInSetupComponents = new DefinedInSetupComponents()
+      const definedInOptionComponents = new DefinedInOptionComponents()
+
       /** @type {Set<string>} */
-      const scriptVariableNames = new Set()
       const scriptTypeOnlyNames = new Set()
       const globalScope = context.getSourceCode().scopeManager.globalScope
       if (globalScope) {
         for (const variable of globalScope.variables) {
-          scriptVariableNames.add(variable.name)
+          definedInSetupComponents.addName(variable.name)
         }
         const moduleScope = globalScope.childScopes.find(
           (scope) => scope.type === 'module'
@@ -146,39 +236,37 @@ module.exports = {
           ) {
             scriptTypeOnlyNames.add(variable.name)
           } else {
-            scriptVariableNames.add(variable.name)
+            definedInSetupComponents.addName(variable.name)
           }
         }
       }
-      /**
-       * @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L334
-       * @param {string} name
-       */
-      const existsSetupReference = (name) => {
-        if (scriptVariableNames.has(name)) {
-          return true
-        }
-        const camelName = camelize(name)
-        if (scriptVariableNames.has(camelName)) {
-          return true
-        }
-        const pascalName = casing.capitalize(camelName)
-        if (scriptVariableNames.has(pascalName)) {
-          return true
+
+      // For circular references
+      const fileName = context.getFilename()
+      const selfComponentName = path.basename(fileName, path.extname(fileName))
+      definedInSetupComponents.addName(selfComponentName)
+      scriptVisitor = utils.defineVueVisitor(context, {
+        onVueObjectEnter(node, { type }) {
+          if (type !== 'export') return
+          const nameProperty = utils.findProperty(node, 'name')
+
+          if (nameProperty && utils.isStringLiteral(nameProperty.value)) {
+            const name = utils.getStringLiteralValue(nameProperty.value)
+            if (name) {
+              definedInOptionComponents.addName(name)
+            }
+          }
         }
-        return false
-      }
+      })
+
       verifyName = (rawName, reportNode) => {
         if (!isVerifyTargetComponent(rawName)) {
           return
         }
-        if (existsSetupReference(rawName)) {
+        if (definedInSetupComponents.isDefinedComponent(rawName)) {
           return
         }
-        // Check namespace
-        // https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L305
-        const dotIndex = rawName.indexOf('.')
-        if (dotIndex > 0 && existsSetupReference(rawName.slice(0, dotIndex))) {
+        if (definedInOptionComponents.isDefinedComponent(rawName)) {
           return
         }
 
@@ -192,26 +280,10 @@ module.exports = {
       }
     } else {
       // For Options API
-
-      /**
-       * All registered components
-       * @type {string[]}
-       */
-      const registeredComponentNames = []
-      /**
-       * All registered components, transformed to kebab-case
-       * @type {string[]}
-       */
-      const registeredComponentKebabCaseNames = []
-
-      /**
-       * All registered components using kebab-case syntax
-       * @type {string[]}
-       */
-      const componentsRegisteredAsKebabCase = []
+      const definedInOptionComponents = new DefinedInOptionComponents()
 
       scriptVisitor = utils.executeOnVue(context, (obj) => {
-        registeredComponentNames.push(
+        definedInOptionComponents.addName(
           ...utils.getRegisteredComponents(obj).map(({ name }) => name)
         )
 
@@ -220,33 +292,16 @@ module.exports = {
         if (nameProperty && utils.isStringLiteral(nameProperty.value)) {
           const name = utils.getStringLiteralValue(nameProperty.value)
           if (name) {
-            registeredComponentNames.push(name)
+            definedInOptionComponents.addName(name)
           }
         }
-
-        registeredComponentKebabCaseNames.push(
-          ...registeredComponentNames.map((name) => casing.kebabCase(name))
-        )
-        componentsRegisteredAsKebabCase.push(
-          ...registeredComponentNames.filter(
-            (name) => name === casing.kebabCase(name)
-          )
-        )
       })
 
       verifyName = (rawName, reportNode) => {
         if (!isVerifyTargetComponent(rawName)) {
           return
         }
-        if (registeredComponentNames.includes(rawName)) {
-          return
-        }
-        const kebabCaseName = casing.kebabCase(rawName)
-        if (
-          registeredComponentKebabCaseNames.includes(kebabCaseName) &&
-          !casing.isPascalCase(rawName)
-        ) {
-          // Component registered as `foo-bar` cannot be used as `FooBar`
+        if (definedInOptionComponents.isDefinedComponent(rawName)) {
           return
         }
 
diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js
new file mode 100644
index 00000000..c15762b3
--- /dev/null
+++ b/lib/rules/padding-lines-in-component-definition.js
@@ -0,0 +1,359 @@
+/**
+ * @author ItMaga <https://github.com/ItMaga>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+/**
+ * @typedef {import('../utils').ComponentProp} ComponentProp
+ * @typedef {import('../utils').GroupName} GroupName
+ */
+
+const utils = require('../utils')
+const { isCommentToken } = require('eslint-utils')
+
+const AvailablePaddingOptions = {
+  Never: 'never',
+  Always: 'always',
+  Ignore: 'ignore'
+}
+const OptionKeys = {
+  BetweenOptions: 'betweenOptions',
+  WithinOption: 'withinOption',
+  BetweenItems: 'betweenItems',
+  WithinEach: 'withinEach',
+  GroupSingleLineProperties: 'groupSingleLineProperties'
+}
+
+/**
+ * @param {Token} node
+ */
+function isComma(node) {
+  return node.type === 'Punctuator' && node.value === ','
+}
+
+/**
+ * @param {string} nodeType
+ */
+function isValidProperties(nodeType) {
+  return ['Property', 'SpreadElement'].includes(nodeType)
+}
+
+/**
+ * Split the source code into multiple lines based on the line delimiters.
+ * @param {string} text Source code as a string.
+ * @returns {string[]} Array of source code lines.
+ */
+function splitLines(text) {
+  return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
+}
+
+/**
+ * @param {any} initialOption
+ * @param {string} optionKey
+ * @private
+ * */
+function parseOption(initialOption, optionKey) {
+  return typeof initialOption === 'string'
+    ? initialOption
+    : initialOption[optionKey]
+}
+
+/**
+ * @param {any} initialOption
+ * @param {string} optionKey
+ * @private
+ * */
+function parseBooleanOption(initialOption, optionKey) {
+  if (typeof initialOption === 'string') {
+    if (initialOption === AvailablePaddingOptions.Always) return true
+    if (initialOption === AvailablePaddingOptions.Never) return false
+  }
+  return initialOption[optionKey]
+}
+
+/**
+ * @param {(Property | SpreadElement)} currentProperty
+ * @param {(Property | SpreadElement)} nextProperty
+ * @param {boolean} option
+ * @returns {boolean}
+ * @private
+ * */
+function needGroupSingleLineProperties(currentProperty, nextProperty, option) {
+  const isSingleCurrentProperty =
+    currentProperty.loc.start.line === currentProperty.loc.end.line
+  const isSingleNextProperty =
+    nextProperty.loc.start.line === nextProperty.loc.end.line
+
+  return isSingleCurrentProperty && isSingleNextProperty && option
+}
+
+module.exports = {
+  meta: {
+    type: 'layout',
+    docs: {
+      description: 'require or disallow padding lines in component definition',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/padding-lines-in-component-definition.html'
+    },
+    fixable: 'whitespace',
+    schema: [
+      {
+        oneOf: [
+          {
+            enum: [
+              AvailablePaddingOptions.Always,
+              AvailablePaddingOptions.Never
+            ]
+          },
+          {
+            type: 'object',
+            additionalProperties: false,
+            properties: {
+              [OptionKeys.BetweenOptions]: {
+                enum: Object.values(AvailablePaddingOptions)
+              },
+              [OptionKeys.WithinOption]: {
+                oneOf: [
+                  {
+                    enum: Object.values(AvailablePaddingOptions)
+                  },
+                  {
+                    type: 'object',
+                    patternProperties: {
+                      '^[a-zA-Z]*$': {
+                        oneOf: [
+                          {
+                            enum: Object.values(AvailablePaddingOptions)
+                          },
+                          {
+                            type: 'object',
+                            properties: {
+                              [OptionKeys.BetweenItems]: {
+                                enum: Object.values(AvailablePaddingOptions)
+                              },
+                              [OptionKeys.WithinEach]: {
+                                enum: Object.values(AvailablePaddingOptions)
+                              }
+                            },
+                            additionalProperties: false
+                          }
+                        ]
+                      }
+                    },
+                    minProperties: 1,
+                    additionalProperties: false
+                  }
+                ]
+              },
+              [OptionKeys.GroupSingleLineProperties]: {
+                type: 'boolean'
+              }
+            }
+          }
+        ]
+      }
+    ],
+    messages: {
+      never: 'Unexpected blank line before this definition.',
+      always: 'Expected blank line before this definition.',
+      groupSingleLineProperties:
+        'Unexpected blank line between single line properties.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const options = context.options[0] || AvailablePaddingOptions.Always
+    const sourceCode = context.getSourceCode()
+
+    /**
+     * @param {(Property | SpreadElement)} currentProperty
+     * @param {(Property | SpreadElement | Token)} nextProperty
+     * @param {RuleFixer} fixer
+     * */
+    function replaceLines(currentProperty, nextProperty, fixer) {
+      const commaToken = sourceCode.getTokenAfter(currentProperty, isComma)
+
+      const start = commaToken ? commaToken.range[1] : currentProperty.range[1]
+      const end = nextProperty.range[0]
+
+      const paddingText = sourceCode.text.slice(start, end)
+      const newText = `\n${splitLines(paddingText).pop()}`
+
+      return fixer.replaceTextRange([start, end], newText)
+    }
+
+    /**
+     * @param {(Property | SpreadElement)} currentProperty
+     * @param {(Property | SpreadElement | Token)} nextProperty
+     * @param {RuleFixer} fixer
+     * @param {number} betweenLinesRange
+     * */
+    function insertLines(
+      currentProperty,
+      nextProperty,
+      fixer,
+      betweenLinesRange
+    ) {
+      const commaToken = sourceCode.getTokenAfter(currentProperty, isComma)
+
+      const lineBeforeNextProperty =
+        sourceCode.lines[nextProperty.loc.start.line - 1]
+      const lastSpaces = /** @type {RegExpExecArray} */ (
+        /^\s*/.exec(lineBeforeNextProperty)
+      )[0]
+
+      const newText = betweenLinesRange === 0 ? `\n\n${lastSpaces}` : '\n'
+      return fixer.insertTextAfter(commaToken || currentProperty, newText)
+    }
+
+    /**
+     * @param {(Property | SpreadElement)[]} properties
+     * @param {any} option
+     * @param {any} nextOption
+     * */
+    function verify(properties, option, nextOption) {
+      const groupSingleLineProperties = parseBooleanOption(
+        options,
+        OptionKeys.GroupSingleLineProperties
+      )
+
+      for (const [i, currentProperty] of properties.entries()) {
+        const nextProperty = properties[i + 1]
+
+        if (nextProperty && option !== AvailablePaddingOptions.Ignore) {
+          const tokenBeforeNext = sourceCode.getTokenBefore(nextProperty, {
+            includeComments: true
+          })
+          const isCommentBefore = isCommentToken(tokenBeforeNext)
+          const reportNode = isCommentBefore ? tokenBeforeNext : nextProperty
+
+          const betweenLinesRange =
+            reportNode.loc.start.line - currentProperty.loc.end.line
+
+          if (
+            needGroupSingleLineProperties(
+              currentProperty,
+              nextProperty,
+              groupSingleLineProperties
+            )
+          ) {
+            if (betweenLinesRange > 1) {
+              context.report({
+                node: reportNode,
+                messageId: 'groupSingleLineProperties',
+                loc: reportNode.loc,
+                fix(fixer) {
+                  return replaceLines(currentProperty, reportNode, fixer)
+                }
+              })
+            }
+            continue
+          }
+
+          if (
+            betweenLinesRange <= 1 &&
+            option === AvailablePaddingOptions.Always
+          ) {
+            context.report({
+              node: reportNode,
+              messageId: 'always',
+              loc: reportNode.loc,
+              fix(fixer) {
+                return insertLines(
+                  currentProperty,
+                  reportNode,
+                  fixer,
+                  betweenLinesRange
+                )
+              }
+            })
+          } else if (
+            betweenLinesRange > 1 &&
+            option === AvailablePaddingOptions.Never
+          ) {
+            context.report({
+              node: reportNode,
+              messageId: 'never',
+              loc: reportNode.loc,
+              fix(fixer) {
+                return replaceLines(currentProperty, reportNode, fixer)
+              }
+            })
+          }
+        }
+
+        if (!nextOption) return
+
+        const name = /** @type {GroupName | null} */ (
+          currentProperty.type === 'Property' &&
+            utils.getStaticPropertyName(currentProperty)
+        )
+        if (!name) continue
+
+        const propertyOption = parseOption(nextOption, name)
+        if (!propertyOption) continue
+
+        const nestedProperties =
+          currentProperty.type === 'Property' &&
+          currentProperty.value.type === 'ObjectExpression' &&
+          currentProperty.value.properties
+        if (!nestedProperties) continue
+
+        verify(
+          nestedProperties,
+          parseOption(propertyOption, OptionKeys.BetweenItems),
+          parseOption(propertyOption, OptionKeys.WithinEach)
+        )
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.defineVueVisitor(context, {
+        onVueObjectEnter(node) {
+          verify(
+            node.properties,
+            parseOption(options, OptionKeys.BetweenOptions),
+            parseOption(options, OptionKeys.WithinOption)
+          )
+        }
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefinePropsEnter(_, props) {
+          const propNodes = /** @type {(Property | SpreadElement)[]} */ (
+            props
+              .filter((prop) => prop.node && isValidProperties(prop.node.type))
+              .map((prop) => prop.node)
+          )
+
+          const withinOption = parseOption(options, OptionKeys.WithinOption)
+          const propsOption = withinOption && parseOption(withinOption, 'props')
+          if (!propsOption) return
+
+          verify(
+            propNodes,
+            parseOption(propsOption, OptionKeys.BetweenItems),
+            parseOption(propsOption, OptionKeys.WithinEach)
+          )
+        },
+        onDefineEmitsEnter(_, emits) {
+          const emitNodes = /** @type {(Property | SpreadElement)[]} */ (
+            emits
+              .filter((emit) => emit.node && isValidProperties(emit.node.type))
+              .map((emit) => emit.node)
+          )
+
+          const withinOption = parseOption(options, OptionKeys.WithinOption)
+          const emitsOption = withinOption && parseOption(withinOption, 'emits')
+          if (!emitsOption) return
+
+          verify(
+            emitNodes,
+            parseOption(emitsOption, OptionKeys.BetweenItems),
+            parseOption(emitsOption, OptionKeys.WithinEach)
+          )
+        }
+      })
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-undef-components.js b/tests/lib/rules/no-undef-components.js
index 5af9468f..f90cb56d 100644
--- a/tests/lib/rules/no-undef-components.js
+++ b/tests/lib/rules/no-undef-components.js
@@ -108,6 +108,49 @@ tester.run('no-undef-components', rule, {
         }
       ]
     },
+    // circular references
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      </script>
+
+      <template>
+        <test />
+      </template>
+      `
+    },
+    {
+      filename: 'FooBar.vue',
+      code: `
+      <script setup>
+      </script>
+
+      <template>
+        <FooBar />
+        <foo-bar />
+      </template>
+      `
+    },
+    {
+      filename: 'FooBar.vue',
+      code: `
+      <script>
+      export default {
+        name: 'BarFoo'
+      }
+      </script>
+      <script setup>
+      </script>
+
+      <template>
+        <FooBar />
+        <foo-bar />
+        <BarFoo />
+        <bar-foo />
+      </template>
+      `
+    },
 
     // options API
     {
diff --git a/tests/lib/rules/padding-lines-in-component-definition.js b/tests/lib/rules/padding-lines-in-component-definition.js
new file mode 100644
index 00000000..a4ed4d3c
--- /dev/null
+++ b/tests/lib/rules/padding-lines-in-component-definition.js
@@ -0,0 +1,1240 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/padding-lines-in-component-definition')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('padding-lines-in-component-definition', rule, {
+  valid: [
+    {
+      filename: 'Never.vue',
+      code: `
+        <script>
+          export default {
+            name: 'Never',
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+            data() {
+                return {
+                    title: '',
+                    message: '',
+                };
+            },
+            customOption: 'custom'
+          };
+        </script>
+      `,
+      options: ['never']
+    },
+    {
+      filename: 'Setup.vue',
+      code: `
+        import { ref, defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'Setup',
+            setup() {
+                const foo = ref('');
+                const bar = ref('');
+
+                return { foo, bar };
+            }
+        })
+        </script>
+      `,
+      options: ['never']
+    },
+    {
+      filename: 'BetweenOptionsNever.vue',
+      code: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'BetweenOptionsNever',
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            }
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'never',
+          withinOption: 'never'
+        }
+      ]
+    },
+    {
+      filename: 'GroupSingleLineProperties.vue',
+      code: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'GroupSingleLineProperties',
+            inheritAttrs: false,
+
+            props: {
+                foo: String,
+                bar: Number
+            }
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: 'never',
+          groupSingleLineProperties: true
+        }
+      ]
+    },
+    {
+      filename: 'WithinOption.vue',
+      code: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'WithinOption',
+
+            props: {
+                foo: {
+                    type: String,
+
+                    required: true,
+                },
+                bar: {
+                    type: Number,
+
+                    required: true,
+                },
+            },
+
+            data: () => ({
+                title: '',
+
+                message: '',
+            }),
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            props: {
+              betweenItems: 'never',
+              withinEach: 'always'
+            },
+            data: {
+              betweenItems: 'always',
+              withinEach: 'never'
+            }
+          }
+        }
+      ]
+    },
+    {
+      filename: 'CustomOptions.vue',
+      code: `
+        <script>
+        export default {
+            name: 'CustomOptions',
+
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+
+            customOption: {
+                getString() {
+                    return '1';
+                },
+
+                getNumber: () => 1,
+            },
+
+            data: () => ({
+                title: '',
+
+                message: '',
+            }),
+        }
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            props: {
+              betweenItems: 'always',
+              withinEach: 'never'
+            },
+            customOption: {
+              betweenItems: 'always'
+            }
+          }
+        }
+      ]
+    },
+    {
+      filename: 'NewVue.js',
+      code: `
+        new Vue({
+          name: 'NewVue',
+          inheritAttrs: false,
+          props: {
+              foo: {
+                  type: String,
+                  required: true,
+              },
+              bar: {
+                  type: String,
+                  required: true,
+              }
+          },
+          customOption: {
+              getString() { return '1' },
+              getNumber: () => 1,
+          },
+        })
+      `,
+      options: ['never']
+    },
+    {
+      filename: 'Mixin.js',
+      code: `
+        Vue.mixin({
+          name: 'Mixin',
+          inheritAttrs: false,
+          props: {
+              foo: {
+                  type: String,
+                  required: true,
+              },
+              bar: {
+                  type: String,
+                  required: true,
+              }
+          },
+          customOption: {
+              getString() { return '1' },
+              getNumber: () => 1,
+          },
+        })
+      `,
+      options: [
+        {
+          betweenOptions: 'never',
+          withinOption: 'ignore',
+          groupSingleLineProperties: true
+        }
+      ]
+    },
+    {
+      filename: 'DefineProps.vue',
+      code: `
+        import { defineProps } from 'vue'
+        <script setup>
+        const props = defineProps({
+            foo: {
+                type: String,
+                required: true,
+            },
+            bar: {
+                type: String,
+                required: true,
+            },
+        })
+        </script>
+      `,
+      options: ['never']
+    },
+    {
+      filename: 'DefineEmits.vue',
+      code: `
+        import { defineEmits } from 'vue'
+        <script setup>
+        const emits = defineEmits(['foo', 'bar']);
+        const emitsObject = defineEmits({
+          change: (id) => typeof id == 'number',
+
+          update: (value) => typeof value == 'string'
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            emits: {
+              betweenItems: 'always'
+            }
+          },
+          groupSingleLineProperties: false
+        }
+      ]
+    },
+    {
+      filename: 'Comment.vue',
+      code: `
+        <script>
+        export default {
+            name: 'Comment',
+
+            props: {
+                a: {
+                    type: String,
+                },
+
+                /** JSDoc */
+                b: {
+                    type: String,
+                },
+
+                // single
+                c: {
+                    type: String,
+                },
+
+                /* block */
+                d: String,
+                /* block */
+                f: String
+            }
+        }
+        </script>
+      `,
+      options: ['always']
+    },
+    {
+      filename: 'Spread.vue',
+      code: `
+        <script>
+        export default {
+            name: 'Spread',
+            ...spread,
+            props: {
+                a: {
+                    type: String,
+                },
+                ...lost,
+                b: {
+                    type: String,
+                }
+            }
+        }
+        </script>
+      `,
+      options: ['never']
+    },
+    {
+      filename: 'SpreadWithComment.vue',
+      code: `
+        <script>
+        export default {
+            name: 'Spread',
+            // comment
+            ...spread,
+
+            props: {
+                a: {
+                    type: String,
+                },
+
+                // comment
+                ...lost,
+
+                b: {
+                    type: String,
+                }
+            }
+        }
+        </script>
+      `,
+      options: ['always']
+    }
+  ],
+  invalid: [
+    {
+      filename: 'Always.vue',
+      code: `
+      <script>
+          export default {
+            name: 'Always',
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+            data() {
+                return {
+                    title: '',
+                    message: '',
+                };
+            },
+          };
+      </script>
+      `,
+      output: `
+      <script>
+          export default {
+            name: 'Always',
+
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+
+            data() {
+                return {
+                    title: '',
+                    message: '',
+                };
+            },
+          };
+      </script>
+      `,
+      options: ['always'],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 5
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 10
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 15
+        }
+      ]
+    },
+    {
+      filename: 'Setup.vue',
+      code: `
+        import { ref, defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'Setup',
+
+            setup() {
+                const foo = ref('');
+                const bar = ref('');
+
+                return { foo, bar };
+            }
+        })
+        </script>
+      `,
+      output: `
+        import { ref, defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'Setup',
+            setup() {
+                const foo = ref('');
+                const bar = ref('');
+
+                return { foo, bar };
+            }
+        })
+        </script>
+      `,
+      options: ['never'],
+      errors: [
+        {
+          message: 'Unexpected blank line before this definition.',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'BetweenOptionsAlways.vue',
+      code: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'BetweenOptionsAlways',
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            }
+        })
+        </script>
+      `,
+      output: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'BetweenOptionsAlways',
+
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            }
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: 'never'
+        }
+      ],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'GroupSingleLineProperties.vue',
+      code: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'GroupSingleLineProperties',
+
+            inheritAttrs: false,
+
+            props: {
+                foo: String,
+                bar: Number
+            }
+        })
+        </script>
+      `,
+      output: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'GroupSingleLineProperties',
+            inheritAttrs: false,
+
+            props: {
+                foo: String,
+                bar: Number
+            }
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: 'never',
+          groupSingleLineProperties: true
+        }
+      ],
+      errors: [
+        {
+          message: 'Unexpected blank line between single line properties.',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'WithinOption.vue',
+      code: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'WithinOption',
+
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+        })
+        </script>
+      `,
+      output: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'WithinOption',
+
+            props: {
+                foo: {
+                    type: String,
+
+                    required: true,
+                },
+
+                bar: {
+                    type: Number,
+
+                    required: true,
+                },
+            },
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            props: {
+              betweenItems: 'always',
+              withinEach: 'always'
+            },
+            data: {
+              betweenItems: 'always',
+              withinEach: 'never'
+            }
+          }
+        }
+      ],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 10
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 12
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 14
+        }
+      ]
+    },
+    {
+      filename: 'CustomOptions.vue',
+      code: `
+        <script>
+        export default {
+            name: 'CustomOptions',
+
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+
+            customOption: {
+                getString() {
+                    return '1';
+                },
+
+                getNumber: () => 1,
+            },
+
+            data: () => ({
+                title: '',
+
+                message: '',
+            }),
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+            name: 'CustomOptions',
+
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+
+            customOption: {
+                getString() {
+                    return '1';
+                },
+                getNumber: () => 1,
+            },
+
+            data: () => ({
+                title: '',
+
+                message: '',
+            }),
+        }
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            props: {
+              betweenItems: 'always',
+              withinEach: 'never'
+            },
+            customOption: {
+              betweenItems: 'never'
+            }
+          }
+        }
+      ],
+      errors: [
+        {
+          message: 'Unexpected blank line before this definition.',
+          line: 23
+        }
+      ]
+    },
+    {
+      filename: 'NewVue.js',
+      code: `
+        new Vue({
+          name: 'NewVue',
+
+          inheritAttrs: false,
+
+          props: {
+              foo: {
+                  type: String,
+                  required: true,
+              },
+              bar: {
+                  type: String,
+                  required: true,
+              }
+          },
+          customOption: {
+              getString() { return '1' },
+              getNumber: () => 1,
+          },
+        })
+      `,
+      output: `
+        new Vue({
+          name: 'NewVue',
+          inheritAttrs: false,
+
+          props: {
+              foo: {
+                  type: String,
+                  required: true,
+              },
+              bar: {
+                  type: String,
+                  required: true,
+              }
+          },
+
+          customOption: {
+              getString() { return '1' },
+              getNumber: () => 1,
+          },
+        })
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: 'ignore',
+          groupSingleLineProperties: true
+        }
+      ],
+      errors: [
+        {
+          message: 'Unexpected blank line between single line properties.',
+          line: 5
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 17
+        }
+      ]
+    },
+    {
+      filename: 'Mixin.js',
+      code: `
+        Vue.mixin({
+          name: 'NewVue',
+          inheritAttrs: false,
+          props: {
+              foo: {
+                  type: String,
+                  required: true,
+              },
+              bar: {
+                  type: String,
+                  required: true,
+              }
+          },
+          customOption: {
+              getString() { return '1' },
+              getNumber: () => 1,
+          },
+        })
+      `,
+      output: `
+        Vue.mixin({
+          name: 'NewVue',
+
+          inheritAttrs: false,
+
+          props: {
+              foo: {
+                  type: String,
+                  required: true,
+              },
+              bar: {
+                  type: String,
+                  required: true,
+              }
+          },
+
+          customOption: {
+              getString() { return '1' },
+              getNumber: () => 1,
+          },
+        })
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: 'ignore',
+          groupSingleLineProperties: false
+        }
+      ],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 4
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 5
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 15
+        }
+      ]
+    },
+    {
+      filename: 'DefineProps.vue',
+      code: `
+        import { defineProps } from 'vue'
+        <script setup>
+        const props = defineProps({
+            foo: {
+                type: String,
+
+                required: true,
+            },
+            bar: {
+                type: String,
+                required: true,
+            },
+        })
+        </script>
+      `,
+      output: `
+        import { defineProps } from 'vue'
+        <script setup>
+        const props = defineProps({
+            foo: {
+                type: String,
+                required: true,
+            },
+
+            bar: {
+                type: String,
+                required: true,
+            },
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            props: {
+              betweenItems: 'always',
+              withinEach: 'never'
+            }
+          }
+        }
+      ],
+      errors: [
+        {
+          message: 'Unexpected blank line before this definition.',
+          line: 8
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 10
+        }
+      ]
+    },
+    {
+      filename: 'DefineEmits.vue',
+      code: `
+        import { defineEmits } from 'vue'
+        <script setup>
+        const emits = defineEmits(['foo', 'bar']);
+        const emitsObject = defineEmits({
+          change: (id) => typeof id == 'number',
+
+          update: (value) => typeof value == 'string'
+        })
+        </script>
+      `,
+      output: `
+        import { defineEmits } from 'vue'
+        <script setup>
+        const emits = defineEmits(['foo', 'bar']);
+        const emitsObject = defineEmits({
+          change: (id) => typeof id == 'number',
+          update: (value) => typeof value == 'string'
+        })
+        </script>
+      `,
+      options: ['never'],
+      errors: [
+        {
+          message: 'Unexpected blank line before this definition.',
+          line: 8
+        }
+      ]
+    },
+    {
+      filename: 'WithinOption.vue',
+      code: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'PropertiesInOneLine',props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+        })
+        </script>
+      `,
+      output: `
+        import { defineComponent } from 'vue';
+        <script>
+        export default defineComponent({
+            name: 'PropertiesInOneLine',
+
+            props: {
+                foo: {
+                    type: String,
+                    required: true,
+                },
+
+                bar: {
+                    type: Number,
+                    required: true,
+                },
+            },
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            props: {
+              betweenItems: 'always',
+              withinEach: 'always'
+            }
+          },
+          groupSingleLineProperties: true
+        }
+      ],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 5
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 9
+        }
+      ]
+    },
+    {
+      filename: 'Comment.vue',
+      code: `
+        <script>
+        export default {
+            name: 'Comment',
+            props: {
+                a: {
+                    type: String,
+                },
+                /** JSDoc */
+                b: {
+                    type: String,
+                },
+                // eslint-disable-next-line padding-lines-in-component-definition
+                c: {
+                    type: String,
+                },
+
+                /* block */
+                d: String,
+
+                /* block */
+                f: String,
+            }
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+            name: 'Comment',
+
+            props: {
+                a: {
+                    type: String,
+                },
+
+                /** JSDoc */
+                b: {
+                    type: String,
+                },
+
+                // eslint-disable-next-line padding-lines-in-component-definition
+                c: {
+                    type: String,
+                },
+
+                /* block */
+                d: String,
+                /* block */
+                f: String,
+            }
+        }
+        </script>
+      `,
+      options: ['always'],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 5
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 9
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 13
+        },
+        {
+          message: 'Unexpected blank line between single line properties.',
+          line: 21
+        }
+      ]
+    },
+    {
+      filename: 'Spread.vue',
+      code: `
+        <script>
+        export default {
+            name: 'Spread',
+            ...spread,
+            props: {
+                a: {
+                    type: String,
+                },
+                ...lost,
+                b: {
+                    type: String,
+                }
+            }
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+            name: 'Spread',
+            ...spread,
+
+            props: {
+                a: {
+                    type: String,
+                },
+
+                ...lost,
+
+                b: {
+                    type: String,
+                }
+            }
+        }
+        </script>
+      `,
+      options: ['always'],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 6
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 10
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 11
+        }
+      ]
+    },
+    {
+      filename: 'DefineWithSpreadAndComment.vue',
+      code: `
+        import { defineEmits, defineProps } from 'vue'
+        <script setup>
+        const emits = defineEmits(['foo', 'bar']);
+        const emitsObject = defineEmits({
+          change: (id) => typeof id == 'number',
+          ...spread,
+          update: (value) => typeof value == 'string'
+        })
+        const props = defineProps({
+            foo: {
+                type: String,
+                required: true,
+            },
+
+            // comment
+            bar: {
+                type: String,
+                required: true,
+            },
+        })
+        </script>
+      `,
+      output: `
+        import { defineEmits, defineProps } from 'vue'
+        <script setup>
+        const emits = defineEmits(['foo', 'bar']);
+        const emitsObject = defineEmits({
+          change: (id) => typeof id == 'number',
+
+          ...spread,
+
+          update: (value) => typeof value == 'string'
+        })
+        const props = defineProps({
+            foo: {
+                type: String,
+                required: true,
+            },
+            // comment
+            bar: {
+                type: String,
+                required: true,
+            },
+        })
+        </script>
+      `,
+      options: [
+        {
+          betweenOptions: 'always',
+          withinOption: {
+            emits: 'always',
+            props: 'never'
+          },
+          groupSingleLineProperties: false
+        }
+      ],
+      errors: [
+        {
+          message: 'Expected blank line before this definition.',
+          line: 7
+        },
+        {
+          message: 'Expected blank line before this definition.',
+          line: 8
+        },
+        {
+          message: 'Unexpected blank line before this definition.',
+          line: 16
+        }
+      ]
+    }
+  ]
+})