Skip to content

Commit a4e807c

Browse files
authored
feat: add no-required-prop-with-default rule (#1943)
* feat: add optional-props-using-with-defaults * feat: improve rule name * feat: change to problem * feat: fix comments * feat: add suggest to rule
1 parent e9964e1 commit a4e807c

File tree

5 files changed

+1175
-0
lines changed

5 files changed

+1175
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ For example:
230230
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | | :hammer: |
231231
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
232232
| [vue/no-ref-object-destructure](./no-ref-object-destructure.md) | disallow destructuring of ref objects that can lead to loss of reactivity | | :warning: |
233+
| [vue/no-required-prop-with-default](./no-required-prop-with-default.md) | enforce props with default values ​​to be optional | :wrench::bulb: | :warning: |
233234
| [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block | | :hammer: |
234235
| [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods | | :hammer: |
235236
| [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components | | :warning: |

Diff for: docs/rules/no-required-prop-with-default.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-required-prop-with-default
5+
description: enforce props with default values ​​to be optional
6+
---
7+
# vue/no-required-prop-with-default
8+
9+
> enforce props with default values ​​to be optional
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
14+
15+
## :book: Rule Details
16+
17+
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.
18+
So, a required prop with a default value is essentially the same as an optional prop.
19+
This rule enforces all props with default values to be optional.
20+
21+
<eslint-code-block fix :rules="{'vue/no-required-prop-with-default': ['error', { autoFix: true }]}">
22+
23+
```vue
24+
<script setup lang="ts">
25+
/* ✓ GOOD */
26+
const props = withDefaults(
27+
defineProps<{
28+
name?: string | number
29+
age?: number
30+
}>(),
31+
{
32+
name: "Foo",
33+
}
34+
);
35+
36+
/* ✗ BAD */
37+
const props = withDefaults(
38+
defineProps<{
39+
name: string | number
40+
age?: number
41+
}>(),
42+
{
43+
name: "Foo",
44+
}
45+
);
46+
</script>
47+
```
48+
49+
</eslint-code-block>
50+
51+
<eslint-code-block fix :rules="{'vue/no-required-prop-with-default': ['error', { autoFix: true }]}">
52+
53+
```vue
54+
<script>
55+
export default {
56+
/* ✓ GOOD */
57+
props: {
58+
name: {
59+
required: true,
60+
default: 'Hello'
61+
}
62+
}
63+
64+
/* ✗ BAD */
65+
props: {
66+
name: {
67+
required: true,
68+
default: 'Hello'
69+
}
70+
}
71+
}
72+
</script>
73+
```
74+
75+
</eslint-code-block>
76+
77+
## :wrench: Options
78+
79+
```json
80+
{
81+
"vue/no-required-prop-with-default": ["error", {
82+
"autofix": false,
83+
}]
84+
}
85+
```
86+
87+
- `"autofix"` ... If `true`, enable autofix. (Default: `false`)
88+
89+
## :couple: Related Rules
90+
91+
- [vue/require-default-prop](./require-default-prop.md)
92+
93+
## :mag: Implementation
94+
95+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-required-prop-with-default.js)
96+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-required-prop-with-default.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ module.exports = {
107107
'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
108108
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
109109
'no-ref-object-destructure': require('./rules/no-ref-object-destructure'),
110+
'no-required-prop-with-default': require('./rules/no-required-prop-with-default'),
110111
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
111112
'no-reserved-keys': require('./rules/no-reserved-keys'),
112113
'no-reserved-props': require('./rules/no-reserved-props'),

Diff for: lib/rules/no-required-prop-with-default.js

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
* @author @neferqiqi
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
// ------------------------------------------------------------------------------
7+
// Requirements
8+
// ------------------------------------------------------------------------------
9+
10+
const utils = require('../utils')
11+
/**
12+
* @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
13+
* @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
14+
* @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
15+
* @typedef {import('../utils').ComponentUnknownProp} ComponentUnknownProp
16+
* @typedef {import('../utils').ComponentProp} ComponentProp
17+
*/
18+
19+
// ------------------------------------------------------------------------------
20+
// Rule Definition
21+
// ------------------------------------------------------------------------------
22+
23+
module.exports = {
24+
meta: {
25+
hasSuggestions: true,
26+
type: 'problem',
27+
docs: {
28+
description: 'enforce props with default values ​​to be optional',
29+
categories: undefined,
30+
url: 'https://eslint.vuejs.org/rules/no-required-prop-with-default.html'
31+
},
32+
fixable: 'code',
33+
schema: [
34+
{
35+
type: 'object',
36+
properties: {
37+
autofix: {
38+
type: 'boolean'
39+
}
40+
},
41+
additionalProperties: false
42+
}
43+
],
44+
messages: {
45+
requireOptional: `Prop "{{ key }}" should be optional.`,
46+
fixRequiredProp: `Change this prop to be optional.`
47+
}
48+
},
49+
/** @param {RuleContext} context */
50+
create(context) {
51+
let canAutoFix = false
52+
const option = context.options[0]
53+
if (option) {
54+
canAutoFix = option.autofix
55+
}
56+
57+
/**
58+
* @param {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp | ComponentProp} prop
59+
* */
60+
const handleObjectProp = (prop) => {
61+
if (
62+
prop.type === 'object' &&
63+
prop.propName &&
64+
prop.value.type === 'ObjectExpression' &&
65+
utils.findProperty(prop.value, 'default')
66+
) {
67+
const requiredProperty = utils.findProperty(prop.value, 'required')
68+
if (!requiredProperty) return
69+
const requiredNode = requiredProperty.value
70+
if (
71+
requiredNode &&
72+
requiredNode.type === 'Literal' &&
73+
!!requiredNode.value
74+
) {
75+
context.report({
76+
node: prop.node,
77+
loc: prop.node.loc,
78+
data: {
79+
key: prop.propName
80+
},
81+
messageId: 'requireOptional',
82+
fix: canAutoFix
83+
? (fixer) => fixer.replaceText(requiredNode, 'false')
84+
: null,
85+
suggest: canAutoFix
86+
? null
87+
: [
88+
{
89+
messageId: 'fixRequiredProp',
90+
fix: (fixer) => fixer.replaceText(requiredNode, 'false')
91+
}
92+
]
93+
})
94+
}
95+
}
96+
}
97+
98+
return utils.compositingVisitors(
99+
utils.defineVueVisitor(context, {
100+
onVueObjectEnter(node) {
101+
utils.getComponentPropsFromOptions(node).map(handleObjectProp)
102+
}
103+
}),
104+
utils.defineScriptSetupVisitor(context, {
105+
onDefinePropsEnter(node, props) {
106+
if (!utils.hasWithDefaults(node)) {
107+
props.map(handleObjectProp)
108+
return
109+
}
110+
const withDefaultsProps = Object.keys(
111+
utils.getWithDefaultsPropExpressions(node)
112+
)
113+
const requiredProps = props.flatMap((item) =>
114+
item.type === 'type' && item.required ? [item] : []
115+
)
116+
117+
for (const prop of requiredProps) {
118+
if (withDefaultsProps.includes(prop.propName)) {
119+
// skip setter & getter case
120+
if (
121+
prop.node.type === 'TSMethodSignature' &&
122+
(prop.node.kind === 'get' || prop.node.kind === 'set')
123+
) {
124+
return
125+
}
126+
// skip computed
127+
if (prop.node.computed) {
128+
return
129+
}
130+
context.report({
131+
node: prop.node,
132+
loc: prop.node.loc,
133+
data: {
134+
key: prop.propName
135+
},
136+
messageId: 'requireOptional',
137+
fix: canAutoFix
138+
? (fixer) => fixer.insertTextAfter(prop.key, '?')
139+
: null,
140+
suggest: canAutoFix
141+
? null
142+
: [
143+
{
144+
messageId: 'fixRequiredProp',
145+
fix: (fixer) => fixer.insertTextAfter(prop.key, '?')
146+
}
147+
]
148+
})
149+
}
150+
}
151+
}
152+
})
153+
)
154+
}
155+
}

0 commit comments

Comments
 (0)