diff --git a/docs/rules/README.md b/docs/rules/README.md
index de00e7e53..b9bf3826e 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -228,6 +228,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/no-required-prop-with-default.md b/docs/rules/no-required-prop-with-default.md
new file mode 100644
index 000000000..92800a0d3
--- /dev/null
+++ b/docs/rules/no-required-prop-with-default.md
@@ -0,0 +1,96 @@
+---
+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: ***This rule has not been released yet.***
+- :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.
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :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 ca3a93084..973564127 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -105,6 +105,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 000000000..6d62b89db
--- /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/tests/lib/rules/no-required-prop-with-default.js b/tests/lib/rules/no-required-prop-with-default.js
new file mode 100644
index 000000000..236de8cd4
--- /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: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ // ignore array prop
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ autofix: true }],
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ autofix: true }],
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "na::me" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "na"me2" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "foo" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "foo" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ autofix: true }],
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "a" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "a" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 6
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 6
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 6
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [{ autofix: true }],
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ }
+ ]
+})