Repository: vuejs/eslint-plugin-vue
Pull request #1982 (2022-09-19 12:49:28)

Add `consistent` option to `vue/padding-line-between-tags`
For #1978 


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index a1659e46..780b10cb 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -230,6 +230,7 @@ For example:
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  | :hammer: |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
 | [vue/no-ref-object-destructure](./no-ref-object-destructure.md) | disallow destructuring of ref objects that can lead to loss of reactivity |  | :warning: |
+| [vue/no-required-prop-with-default](./no-required-prop-with-default.md) | enforce props with default values ​​to be optional | :wrench::bulb: | :warning: |
 | [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block |  | :hammer: |
 | [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods |  | :hammer: |
 | [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components |  | :warning: |
diff --git a/docs/rules/define-emits-declaration.md b/docs/rules/define-emits-declaration.md
index 087f2b69..2fd4b478 100644
--- a/docs/rules/define-emits-declaration.md
+++ b/docs/rules/define-emits-declaration.md
@@ -25,6 +25,12 @@ const emit = defineEmits<{
   (e: 'update', value: string): void
 }>()
 
+/* ✗ BAD */
+const emit = defineEmits({
+  change: (id) => typeof id == 'number',
+  update: (value) => typeof value == 'string'
+})
+
 /* ✗ BAD */
 const emit = defineEmits(['change', 'update'])
 </script>
@@ -53,6 +59,12 @@ const emit = defineEmits<{
   (e: 'update', value: string): void
 }>()
 
+/* ✓ GOOD */
+const emit = defineEmits({
+  change: (id) => typeof id == 'number',
+  update: (value) => typeof value == 'string'
+})
+
 /* ✓ GOOD */
 const emit = defineEmits(['change', 'update'])
 </script>
diff --git a/docs/rules/define-props-declaration.md b/docs/rules/define-props-declaration.md
index 6d8d2e8d..8b88f1ce 100644
--- a/docs/rules/define-props-declaration.md
+++ b/docs/rules/define-props-declaration.md
@@ -21,12 +21,14 @@ This rule only works in setup script and `lang="ts"`.
 <script setup lang="ts">
 /* ✓ GOOD */
 const props = defineProps<{
-  kind: string
+  kind: string,
+  options: { title: string }
 }>()
 
 /* ✗ BAD */
 const props = defineProps({
-  kind: { type: String }
+  kind: { type: String },
+  options: { type: Object as PropType<{ title: string }> }
 })
 </script>
 ```
@@ -50,12 +52,14 @@ const props = defineProps({
 <script setup lang="ts">
 /* ✓ GOOD */
 const props = defineProps({
-  kind: { type: String }
+  kind: { type: String },
+  options: { type: Object as PropType<{ title: string }> }
 })
 
 /* ✗ BAD */
 const props = defineProps<{
-  kind: string
+  kind: string,
+  options: { title: string }
 }>()
 </script>
 ```
diff --git a/docs/rules/no-required-prop-with-default.md b/docs/rules/no-required-prop-with-default.md
new file mode 100644
index 00000000..418a4118
--- /dev/null
+++ b/docs/rules/no-required-prop-with-default.md
@@ -0,0 +1,105 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-required-prop-with-default
+description: enforce props with default values ​​to be optional
+---
+# vue/no-required-prop-with-default
+
+> enforce props with default values ​​to be optional
+
+- :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.
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+## :book: Rule Details
+
+If a prop is declared with a default value, whether it is required or not, we can always skip it in actual use. In that situation, the default value would be applied.
+So, a required prop with a default value is essentially the same as an optional prop.
+This rule enforces all props with default values to be optional.
+
+<eslint-code-block fix :rules="{'vue/no-required-prop-with-default': ['error', { autoFix: true }]}">
+
+```vue
+<script setup lang="ts">
+  /* ✓ GOOD */
+  const props = withDefaults(
+    defineProps<{
+      name?: string | number
+      age?: number
+    }>(),
+    {
+      name: "Foo",
+    }
+  );
+
+  /* ✗ BAD */
+  const props = withDefaults(
+    defineProps<{
+      name: string | number
+      age?: number
+    }>(),
+    {
+      name: "Foo",
+    }
+  );
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/no-required-prop-with-default': ['error', { autoFix: true }]}">
+
+```vue
+<script>
+  export default {
+    /* ✓ GOOD */
+    props: {
+      name: {
+        required: true,
+        default: 'Hello'
+      }
+    }
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/no-required-prop-with-default': ['error', { autoFix: true }]}">
+
+```vue
+<script>
+  export default {
+    /* ✗ BAD */
+    props: {
+      name: {
+        required: true,
+        default: 'Hello'
+      }
+    }
+  }
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/no-required-prop-with-default": ["error", {
+    "autofix": false,
+  }]
+}
+```
+
+- `"autofix"` ... If `true`, enable autofix. (Default: `false`)
+
+## :couple: Related Rules
+
+- [vue/require-default-prop](./require-default-prop.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-required-prop-with-default.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-required-prop-with-default.js)
diff --git a/lib/index.js b/lib/index.js
index 6eba5f88..e7dcd5be 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -107,6 +107,7 @@ module.exports = {
     'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
     'no-ref-as-operand': require('./rules/no-ref-as-operand'),
     'no-ref-object-destructure': require('./rules/no-ref-object-destructure'),
+    'no-required-prop-with-default': require('./rules/no-required-prop-with-default'),
     'no-reserved-component-names': require('./rules/no-reserved-component-names'),
     'no-reserved-keys': require('./rules/no-reserved-keys'),
     'no-reserved-props': require('./rules/no-reserved-props'),
diff --git a/lib/rules/no-required-prop-with-default.js b/lib/rules/no-required-prop-with-default.js
new file mode 100644
index 00000000..6d62b89d
--- /dev/null
+++ b/lib/rules/no-required-prop-with-default.js
@@ -0,0 +1,155 @@
+/**
+ * @author @neferqiqi
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+/**
+ * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
+ * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
+ * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
+ * @typedef {import('../utils').ComponentUnknownProp} ComponentUnknownProp
+ * @typedef {import('../utils').ComponentProp} ComponentProp
+ */
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    hasSuggestions: true,
+    type: 'problem',
+    docs: {
+      description: 'enforce props with default values ​​to be optional',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-required-prop-with-default.html'
+    },
+    fixable: 'code',
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          autofix: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      requireOptional: `Prop "{{ key }}" should be optional.`,
+      fixRequiredProp: `Change this prop to be optional.`
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    let canAutoFix = false
+    const option = context.options[0]
+    if (option) {
+      canAutoFix = option.autofix
+    }
+
+    /**
+     * @param {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp | ComponentProp} prop
+     * */
+    const handleObjectProp = (prop) => {
+      if (
+        prop.type === 'object' &&
+        prop.propName &&
+        prop.value.type === 'ObjectExpression' &&
+        utils.findProperty(prop.value, 'default')
+      ) {
+        const requiredProperty = utils.findProperty(prop.value, 'required')
+        if (!requiredProperty) return
+        const requiredNode = requiredProperty.value
+        if (
+          requiredNode &&
+          requiredNode.type === 'Literal' &&
+          !!requiredNode.value
+        ) {
+          context.report({
+            node: prop.node,
+            loc: prop.node.loc,
+            data: {
+              key: prop.propName
+            },
+            messageId: 'requireOptional',
+            fix: canAutoFix
+              ? (fixer) => fixer.replaceText(requiredNode, 'false')
+              : null,
+            suggest: canAutoFix
+              ? null
+              : [
+                  {
+                    messageId: 'fixRequiredProp',
+                    fix: (fixer) => fixer.replaceText(requiredNode, 'false')
+                  }
+                ]
+          })
+        }
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.defineVueVisitor(context, {
+        onVueObjectEnter(node) {
+          utils.getComponentPropsFromOptions(node).map(handleObjectProp)
+        }
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefinePropsEnter(node, props) {
+          if (!utils.hasWithDefaults(node)) {
+            props.map(handleObjectProp)
+            return
+          }
+          const withDefaultsProps = Object.keys(
+            utils.getWithDefaultsPropExpressions(node)
+          )
+          const requiredProps = props.flatMap((item) =>
+            item.type === 'type' && item.required ? [item] : []
+          )
+
+          for (const prop of requiredProps) {
+            if (withDefaultsProps.includes(prop.propName)) {
+              // skip setter & getter case
+              if (
+                prop.node.type === 'TSMethodSignature' &&
+                (prop.node.kind === 'get' || prop.node.kind === 'set')
+              ) {
+                return
+              }
+              // skip computed
+              if (prop.node.computed) {
+                return
+              }
+              context.report({
+                node: prop.node,
+                loc: prop.node.loc,
+                data: {
+                  key: prop.propName
+                },
+                messageId: 'requireOptional',
+                fix: canAutoFix
+                  ? (fixer) => fixer.insertTextAfter(prop.key, '?')
+                  : null,
+                suggest: canAutoFix
+                  ? null
+                  : [
+                      {
+                        messageId: 'fixRequiredProp',
+                        fix: (fixer) => fixer.insertTextAfter(prop.key, '?')
+                      }
+                    ]
+              })
+            }
+          }
+        }
+      })
+    )
+  }
+}
diff --git a/lib/rules/padding-line-between-tags.js b/lib/rules/padding-line-between-tags.js
index 3e0994a1..a6144d9a 100644
--- a/lib/rules/padding-line-between-tags.js
+++ b/lib/rules/padding-line-between-tags.js
@@ -24,44 +24,75 @@ function splitLines(text) {
  * @param {RuleContext} context
  * @param {VElement} tag
  * @param {VElement} sibling
+ * @param {number} lineDifference
  */
-function insertNewLine(context, tag, sibling) {
-  context.report({
-    messageId: 'always',
-    loc: sibling.loc,
-    // @ts-ignore
-    fix(fixer) {
-      return fixer.insertTextAfter(tag, '\n')
-    }
-  })
+function insertNewLine(context, tag, sibling, lineDifference) {
+  const endTag = tag.endTag || tag.startTag
+
+  if (lineDifference === 1) {
+    context.report({
+      messageId: 'always',
+      loc: sibling.loc,
+      // @ts-ignore
+      fix(fixer) {
+        return fixer.insertTextAfter(tag, '\n')
+      }
+    })
+  } else if (lineDifference === 0) {
+    context.report({
+      messageId: 'always',
+      loc: sibling.loc,
+      // @ts-ignore
+      fix(fixer) {
+        const lastSpaces = /** @type {RegExpExecArray} */ (
+          /^\s*/.exec(context.getSourceCode().lines[endTag.loc.start.line - 1])
+        )[0]
+
+        return fixer.insertTextAfter(endTag, `\n\n${lastSpaces}`)
+      }
+    })
+  }
 }
 
 /**
  * @param {RuleContext} context
  * @param {VEndTag | VStartTag} endTag
  * @param {VElement} sibling
+ * @param {number} lineDifference
  */
-function removeExcessLines(context, endTag, sibling) {
-  context.report({
-    messageId: 'never',
-    loc: sibling.loc,
-    // @ts-ignore
-    fix(fixer) {
-      const start = endTag.range[1]
-      const end = sibling.range[0]
-      const paddingText = context.getSourceCode().text.slice(start, end)
-      const textBetween = splitLines(paddingText)
-      let newTextBetween = `\n${textBetween.pop()}`
-      for (let i = textBetween.length - 1; i >= 0; i--) {
-        if (!/^\s*$/.test(textBetween[i])) {
-          newTextBetween = `${i === 0 ? '' : '\n'}${
-            textBetween[i]
-          }${newTextBetween}`
+function removeExcessLines(context, endTag, sibling, lineDifference) {
+  if (lineDifference > 1) {
+    let hasOnlyTextBetween = true
+    for (
+      let i = endTag.loc.start.line;
+      i < sibling.loc.start.line - 1 && hasOnlyTextBetween;
+      i++
+    ) {
+      hasOnlyTextBetween = !/^\s*$/.test(context.getSourceCode().lines[i])
+    }
+    if (!hasOnlyTextBetween) {
+      context.report({
+        messageId: 'never',
+        loc: sibling.loc,
+        // @ts-ignore
+        fix(fixer) {
+          const start = endTag.range[1]
+          const end = sibling.range[0]
+          const paddingText = context.getSourceCode().text.slice(start, end)
+          const textBetween = splitLines(paddingText)
+          let newTextBetween = `\n${textBetween.pop()}`
+          for (let i = textBetween.length - 1; i >= 0; i--) {
+            if (!/^\s*$/.test(textBetween[i])) {
+              newTextBetween = `${i === 0 ? '' : '\n'}${
+                textBetween[i]
+              }${newTextBetween}`
+            }
+          }
+          return fixer.replaceTextRange([start, end], `${newTextBetween}`)
         }
-      }
-      return fixer.replaceTextRange([start, end], `${newTextBetween}`)
+      })
     }
-  })
+  }
 }
 
 // ------------------------------------------------------------------------------
@@ -72,11 +103,19 @@ function removeExcessLines(context, endTag, sibling) {
  * @param {RuleContext} context
  */
 function checkNewline(context) {
-  /** @type {Array<{blankLine: "always" | "never", prev: string, next: string}>} */
+  /** @type {Array<{blankLine: "always" | "never" | "consistent", prev: string, next: string}>} */
   const configureList = context.options[0] || [
     { blankLine: 'always', prev: '*', next: '*' }
   ]
 
+  const reverseConfigureList = [...configureList].reverse()
+
+  /**
+   * It has the style of the first `blankLine="consistent"`.
+   * @type {Map<VElement, "always" | "never">}
+   */
+  const firstConsistentBlankLines = new Map()
+
   /**
    * @param {VElement} block
    */
@@ -99,53 +138,36 @@ function checkNewline(context) {
 
     const closestSibling = /** @type {VElement} */ (lowerSiblings[0])
 
-    for (let i = configureList.length - 1; i >= 0; --i) {
-      const configure = configureList[i]
-      const matched =
+    const configure = reverseConfigureList.find(
+      (configure) =>
         (configure.prev === '*' || block.name === configure.prev) &&
         (configure.next === '*' || closestSibling.name === configure.next)
+    )
 
-      if (matched) {
-        const lineDifference =
-          closestSibling.loc.start.line - endTag.loc.end.line
-        if (configure.blankLine === 'always') {
-          if (lineDifference === 1) {
-            insertNewLine(context, block, closestSibling)
-          } else if (lineDifference === 0) {
-            context.report({
-              messageId: 'always',
-              loc: closestSibling.loc,
-              // @ts-ignore
-              fix(fixer) {
-                const lastSpaces = /** @type {RegExpExecArray} */ (
-                  /^\s*/.exec(
-                    context.getSourceCode().lines[endTag.loc.start.line - 1]
-                  )
-                )[0]
-
-                return fixer.insertTextAfter(endTag, `\n\n${lastSpaces}`)
-              }
-            })
-          }
-        } else {
-          if (lineDifference > 1) {
-            let hasOnlyTextBetween = true
-            for (
-              let i = endTag.loc.start.line;
-              i < closestSibling.loc.start.line - 1 && hasOnlyTextBetween;
-              i++
-            ) {
-              hasOnlyTextBetween = !/^\s*$/.test(
-                context.getSourceCode().lines[i]
-              )
-            }
-            if (!hasOnlyTextBetween) {
-              removeExcessLines(context, endTag, closestSibling)
-            }
-          }
-        }
-        break
+    if (!configure) {
+      return
+    }
+    const lineDifference = closestSibling.loc.start.line - endTag.loc.end.line
+
+    let blankLine = configure.blankLine
+    if (blankLine === 'consistent') {
+      const firstConsistentBlankLine = firstConsistentBlankLines.get(
+        block.parent
+      )
+      if (firstConsistentBlankLine == null) {
+        firstConsistentBlankLines.set(
+          block.parent,
+          lineDifference > 1 ? 'always' : 'never'
+        )
+        return
       }
+      blankLine = firstConsistentBlankLine
+    }
+
+    if (blankLine === 'always') {
+      insertNewLine(context, block, closestSibling, lineDifference)
+    } else {
+      removeExcessLines(context, endTag, closestSibling, lineDifference)
     }
   }
 }
@@ -166,7 +188,7 @@ module.exports = {
         items: {
           type: 'object',
           properties: {
-            blankLine: { enum: ['always', 'never'] },
+            blankLine: { enum: ['always', 'never', 'consistent'] },
             prev: { type: 'string' },
             next: { type: 'string' }
           },


Test patch:
diff --git a/tests/lib/rules/no-required-prop-with-default.js b/tests/lib/rules/no-required-prop-with-default.js
new file mode 100644
index 00000000..236de8cd
--- /dev/null
+++ b/tests/lib/rules/no-required-prop-with-default.js
@@ -0,0 +1,922 @@
+/**
+ * @author neferqiqi
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-required-prop-with-default')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-required-prop-with-default', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name?: string
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          type TestPropType = {
+            name?: string
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name?
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            get name(): string
+            set name(a: string)
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'name': 'World',
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            get name(): void
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'name': 'World',
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          const [name] = 'test'
+
+          interface TestPropType {
+            [name]: string
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              [name]: 'World'
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          props: {
+            name: {
+              required: false,
+              default: 'Hello'
+            }
+          }
+        }
+        </script>
+      `
+    },
+    // ignore array prop
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          props: ['name']
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const props = defineProps({
+            name: {
+              required: false,
+              default: 'Hello'
+            }
+          })
+        </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name: string
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name?: string
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name: string | number
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name?: string | number
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            'na::me': string
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'na::me': "World",
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            'na::me'?: string
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'na::me': "World",
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "na::me" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          import nameType from 'name.ts';
+          interface TestPropType {
+            name: nameType
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          import nameType from 'name.ts';
+          interface TestPropType {
+            name?: nameType
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name?
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            name?
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: "World",
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            'na\\"me2'
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'na\\"me2': "World",
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            'na\\"me2'?
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'na\\"me2': "World",
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "na"me2" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            foo(): void
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              foo() {console.log(123)},
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            foo?(): void
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              foo() {console.log(123)},
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "foo" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly foo(): void
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              foo() {console.log(123)},
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly foo?(): void
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              foo() {console.log(123)},
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "foo" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly name
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: 'World',
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly name?
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              name: 'World',
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly 'name'
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'name': 'World',
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly 'name'?
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'name': 'World',
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly 'a'
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              '\\u0061': 'World',
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly 'a'?
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              '\\u0061': 'World',
+            }
+          );
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "a" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly '\\u0061'
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'a': 'World',
+            }
+          );
+        </script>
+      `,
+      output: `
+        <script setup lang="ts">
+          interface TestPropType {
+            readonly '\\u0061'?
+            age?: number
+          }
+          const props = withDefaults(
+            defineProps<TestPropType>(),
+            {
+              'a': 'World',
+            }
+          );
+        </script>
+      `,
+      options: [{ autofix: true }],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Prop "a" should be optional.',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          props: {
+            name: {
+              required: true,
+              default: 'Hello'
+            }
+          }
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+          props: {
+            name: {
+              required: false,
+              default: 'Hello'
+            }
+          }
+        }
+        </script>
+      `,
+      options: [{ autofix: true }],
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          props: {
+            'name': {
+              required: true,
+              default: 'Hello'
+            }
+          }
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+          props: {
+            'name': {
+              required: false,
+              default: 'Hello'
+            }
+          }
+        }
+        </script>
+      `,
+      options: [{ autofix: true }],
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        import { defineComponent } from 'vue'
+        export default defineComponent({
+          props: {
+            'name': {
+              required: true,
+              default: 'Hello'
+            }
+          }
+        })
+        </script>
+      `,
+      output: `
+        <script>
+        import { defineComponent } from 'vue'
+        export default defineComponent({
+          props: {
+            'name': {
+              required: false,
+              default: 'Hello'
+            }
+          }
+        })
+        </script>
+      `,
+      options: [{ autofix: true }],
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        import { defineComponent } from 'vue'
+        export default defineComponent({
+          props: {
+            name: {
+              required: true,
+              default: 'Hello'
+            }
+          }
+        })
+        </script>
+      `,
+      output: `
+        <script>
+        import { defineComponent } from 'vue'
+        export default defineComponent({
+          props: {
+            name: {
+              required: false,
+              default: 'Hello'
+            }
+          }
+        })
+        </script>
+      `,
+      options: [{ autofix: true }],
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        import { defineComponent } from 'vue'
+        export default defineComponent({
+          props: {
+            name: {
+              required: true,
+              default: 'Hello'
+            }
+          }
+        })
+        </script>
+      `,
+      output: `
+        <script>
+        import { defineComponent } from 'vue'
+        export default defineComponent({
+          props: {
+            name: {
+              required: true,
+              default: 'Hello'
+            }
+          }
+        })
+        </script>
+      `,
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const props = defineProps({
+            name: {
+              required: true,
+              default: 'Hello'
+            }
+          })
+        </script>
+      `,
+      output: `
+        <script setup>
+          const props = defineProps({
+            name: {
+              required: false,
+              default: 'Hello'
+            }
+          })
+        </script>
+      `,
+      options: [{ autofix: true }],
+      errors: [
+        {
+          message: 'Prop "name" should be optional.',
+          line: 4
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/padding-line-between-tags.js b/tests/lib/rules/padding-line-between-tags.js
index 18d6743a..67aa6b22 100644
--- a/tests/lib/rules/padding-line-between-tags.js
+++ b/tests/lib/rules/padding-line-between-tags.js
@@ -17,6 +17,96 @@ const tester = new RuleTester({
 
 tester.run('padding-line-between-tags', rule, {
   valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div />
+
+          <div />
+
+          <br/>
+          <br/>
+          <br/>
+          <br/>
+          <br/>
+          <br/>
+
+          <div />
+
+          <div />
+        </div>
+      </template>
+      `,
+      options: [
+        [
+          { blankLine: 'consistent', prev: '*', next: '*' },
+          { blankLine: 'never', prev: 'br', next: 'br' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div />
+        </div>
+      </template>
+      `,
+      options: [[{ blankLine: 'consistent', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div />
+
+          <div />
+        </div>
+      </template>
+      `,
+      options: [[{ blankLine: 'consistent', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <header>
+          <div></div>
+
+          <div></div>
+
+          <div></div>
+        </header>
+        <div></div>
+        <div />
+        <footer></footer>
+      </template>
+      `,
+      options: [[{ blankLine: 'consistent', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <header>
+          <div></div>
+          <div></div>
+          <div></div>
+        </header>
+
+        <div></div>
+
+        <div />
+
+        <footer></footer>
+      </template>
+      `,
+      options: [[{ blankLine: 'consistent', prev: '*', next: '*' }]]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -1027,6 +1117,185 @@ tester.run('padding-line-between-tags', rule, {
         }
       ],
       options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <header>
+          <div></div>
+
+          <div></div>
+          <div></div>
+        </header>
+        <div></div>
+        <div />
+        <footer></footer>
+      </template>
+      `,
+      output: `
+      <template>
+        <header>
+          <div></div>
+
+          <div></div>
+
+          <div></div>
+        </header>
+        <div></div>
+        <div />
+        <footer></footer>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 7,
+          column: 11
+        }
+      ],
+      options: [[{ blankLine: 'consistent', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <header>
+          <div></div>
+
+          <div></div>
+          <div></div>
+          <div></div>
+          <div></div>
+        </header>
+        <div></div>
+        <div />
+        <footer></footer>
+      </template>
+      `,
+      output: `
+      <template>
+        <header>
+          <div></div>
+
+          <div></div>
+
+          <div></div>
+
+          <div></div>
+
+          <div></div>
+        </header>
+        <div></div>
+        <div />
+        <footer></footer>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 7,
+          column: 11
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 8,
+          column: 11
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 9,
+          column: 11
+        }
+      ],
+      options: [[{ blankLine: 'consistent', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div />
+          <div />
+
+          <br/>
+          <br/>
+
+          <div />
+          <div />
+
+          <div />
+          <div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <div />
+          <div />
+          <br/>
+          <br/>
+          <div />
+          <div />
+          <div />
+          <div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 7,
+          column: 11
+        },
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 10,
+          column: 11
+        },
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 13,
+          column: 11
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'consistent', prev: '*', next: '*' },
+          { blankLine: 'never', prev: 'br', next: 'br' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div />
+          <div />
+
+          <div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <div />
+          <div />
+          <div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 7,
+          column: 11
+        }
+      ],
+      options: [[{ blankLine: 'consistent', prev: '*', next: '*' }]]
     }
   ]
 })