Skip to content

Commit eeece27

Browse files
committed
feat: add optional-props-using-with-defaults
1 parent 8828bbb commit eeece27

5 files changed

+1130
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ For example:
249249
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | :hammer: |
250250
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | :hammer: |
251251
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
252+
| [vue/prefer-optional-props-using-with-defaults](./prefer-optional-props-using-with-defaults.md) | enforce props with default values ​​to be optional | :wrench: | :hammer: |
252253
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
253254
| [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: |
254255
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/prefer-optional-props-using-with-defaults
5+
description: enforce props with default values ​​to be optional
6+
---
7+
# vue/prefer-optional-props-using-with-defaults
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+
14+
## :book: Rule Details
15+
16+
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.
17+
So, a required prop with a default value is essentially the same as an optional prop.
18+
This rule enforces all props with default values to be optional.
19+
20+
<eslint-code-block fix :rules="{'vue/prefer-optional-props-using-with-defaults': ['error', { autoFix: true }]}">
21+
22+
```vue
23+
<script setup lang="ts">
24+
/* ✓ GOOD */
25+
const props = withDefaults(
26+
defineProps<{
27+
name?: string | number
28+
age?: number
29+
}>(),
30+
{
31+
name: "Foo",
32+
}
33+
);
34+
35+
/* ✗ BAD */
36+
const props = withDefaults(
37+
defineProps<{
38+
name: string | number
39+
age?: number
40+
}>(),
41+
{
42+
name: "Foo",
43+
}
44+
);
45+
</script>
46+
```
47+
48+
</eslint-code-block>
49+
50+
<eslint-code-block fix :rules="{'vue/prefer-optional-props-using-with-defaults': ['error', { autoFix: true }]}">
51+
52+
```vue
53+
<script setup lang="ts">
54+
export default {
55+
/* ✓ GOOD */
56+
props: {
57+
name: {
58+
required: true,
59+
default: 'Hello'
60+
}
61+
}
62+
63+
/* ✗ BAD */
64+
props: {
65+
name: {
66+
required: true,
67+
default: 'Hello'
68+
}
69+
}
70+
}
71+
</script>
72+
```
73+
74+
</eslint-code-block>
75+
76+
## :wrench: Options
77+
78+
```json
79+
{
80+
"vue/prefer-optional-props-using-with-defaults": ["error", {
81+
"autofix": false,
82+
}]
83+
}
84+
```
85+
86+
- `"autofix"` ... If `true`, enable autofix.
87+
88+
## :couple: Related Rules
89+
90+
- [vue/require-default-prop](./require-default-prop.md)
91+
92+
## :mag: Implementation
93+
94+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-optional-props-using-with-defaults.js)
95+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-optional-props-using-with-defaults.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ module.exports = {
158158
'order-in-components': require('./rules/order-in-components'),
159159
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
160160
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
161+
'prefer-optional-props-using-with-defaults': require('./rules/prefer-optional-props-using-with-defaults'),
161162
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
162163
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
163164
'prefer-template': require('./rules/prefer-template'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
*/
14+
15+
// ------------------------------------------------------------------------------
16+
// Helpers
17+
// ------------------------------------------------------------------------------
18+
19+
// ...
20+
21+
// ------------------------------------------------------------------------------
22+
// Rule Definition
23+
// ------------------------------------------------------------------------------
24+
25+
module.exports = {
26+
meta: {
27+
type: 'suggestion',
28+
docs: {
29+
description: 'enforce props with default values ​​to be optional',
30+
categories: undefined,
31+
url: 'https://eslint.vuejs.org/rules/prefer-optional-props-using-with-defaults.html'
32+
},
33+
fixable: 'code',
34+
schema: [
35+
{
36+
type: 'object',
37+
properties: {
38+
autofix: {
39+
type: 'boolean'
40+
}
41+
},
42+
additionalProperties: false
43+
}
44+
],
45+
messages: {
46+
// ...
47+
}
48+
},
49+
/** @param {RuleContext} context */
50+
create(context) {
51+
/**
52+
* @param {ComponentTypeProp} prop
53+
* @param {Token[]} tokens
54+
* */
55+
const findKeyToken = (prop, tokens) =>
56+
tokens.find((token) => {
57+
const isKeyIdentifierEqual =
58+
prop.key.type === 'Identifier' && token.value === prop.key.name
59+
const isKeyLiteralEqual =
60+
prop.key.type === 'Literal' && token.value === prop.key.raw
61+
return isKeyIdentifierEqual || isKeyLiteralEqual
62+
})
63+
64+
let canAutoFix = false
65+
const option = context.options[0]
66+
if (option) {
67+
canAutoFix = option.autofix
68+
}
69+
70+
return utils.compositingVisitors(
71+
utils.defineVueVisitor(context, {
72+
onVueObjectEnter(node) {
73+
utils.getComponentPropsFromOptions(node).map((prop) => {
74+
if (
75+
prop.type === 'object' &&
76+
prop.propName &&
77+
prop.value.type === 'ObjectExpression' &&
78+
utils.findProperty(prop.value, 'default')
79+
) {
80+
const requiredProperty = utils.findProperty(
81+
prop.value,
82+
'required'
83+
)
84+
if (!requiredProperty) return
85+
const requiredNode = requiredProperty.value
86+
if (
87+
requiredNode &&
88+
requiredNode.type === 'Literal' &&
89+
!!requiredNode.value
90+
) {
91+
context.report({
92+
node: prop.node,
93+
loc: prop.node.loc,
94+
data: {
95+
key: prop.propName
96+
},
97+
message: `Prop "{{ key }}" should be optional.`,
98+
fix: canAutoFix
99+
? (fixer) => fixer.replaceText(requiredNode, 'false')
100+
: null
101+
})
102+
}
103+
}
104+
})
105+
}
106+
}),
107+
utils.defineScriptSetupVisitor(context, {
108+
onDefinePropsEnter(node, props) {
109+
if (!utils.hasWithDefaults(node)) {
110+
return
111+
}
112+
const withDefaultsProps = Object.keys(
113+
utils.getWithDefaultsPropExpressions(node)
114+
)
115+
const requiredProps = props.flatMap((item) =>
116+
item.type === 'type' && item.required ? [item] : []
117+
)
118+
119+
for (const prop of requiredProps) {
120+
if (withDefaultsProps.includes(prop.propName)) {
121+
const firstToken = context
122+
.getSourceCode()
123+
.getFirstToken(prop.node)
124+
// skip setter & getter case
125+
if (firstToken.value === 'get' || firstToken.value === 'set') {
126+
return
127+
}
128+
// skip computed
129+
if (prop.node.computed) {
130+
return
131+
}
132+
const keyToken = findKeyToken(
133+
prop,
134+
context.getSourceCode().getTokens(prop.node)
135+
)
136+
if (!keyToken) return
137+
context.report({
138+
node: prop.node,
139+
loc: prop.node.loc,
140+
data: {
141+
key: prop.propName
142+
},
143+
message: `Prop "{{ key }}" should be optional.`,
144+
fix: canAutoFix
145+
? (fixer) => fixer.insertTextAfter(keyToken, '?')
146+
: null
147+
})
148+
}
149+
}
150+
}
151+
})
152+
)
153+
}
154+
}

0 commit comments

Comments
 (0)