Skip to content

Commit 39bcb2b

Browse files
committed
feat: add optional-props-using-with-defaults
1 parent f358817 commit 39bcb2b

5 files changed

+819
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ For example:
248248
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | :hammer: |
249249
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | :hammer: |
250250
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
251+
| [vue/prefer-optional-props-using-with-defaults](./prefer-optional-props-using-with-defaults.md) | enforce props with default values ​​to be optional | :wrench: | :hammer: |
251252
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
252253
| [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: |
253254
| [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,56 @@
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+
This rule enforce props with default values ​​to be optional.
17+
Because when a required prop declared with a default value, but it doesn't be passed value when using it, it will be assigned the default value. So a required prop with default value is same as a optional prop.
18+
19+
<eslint-code-block fix :rules="{'vue/prefer-optional-props-using-with-defaults': ['error']}">
20+
21+
```vue
22+
<script setup lang="ts">
23+
/* ✓ GOOD */
24+
const props = withDefaults(
25+
defineProps<{
26+
name?: string | number
27+
age?: number
28+
}>(),
29+
{
30+
name: "Foo",
31+
}
32+
);
33+
34+
/* ✗ BAD */
35+
const props = withDefaults(
36+
defineProps<{
37+
name: string | number
38+
age?: number
39+
}>(),
40+
{
41+
name: "Foo",
42+
}
43+
);
44+
</script>
45+
```
46+
47+
</eslint-code-block>
48+
49+
## :wrench: Options
50+
51+
Nothing.
52+
53+
## :mag: Implementation
54+
55+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-optional-props-using-with-defaults.js)
56+
- [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
@@ -157,6 +157,7 @@ module.exports = {
157157
'order-in-components': require('./rules/order-in-components'),
158158
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
159159
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
160+
'prefer-optional-props-using-with-defaults': require('./rules/prefer-optional-props-using-with-defaults'),
160161
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
161162
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
162163
'prefer-template': require('./rules/prefer-template'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
messages: {
36+
// ...
37+
}
38+
},
39+
/** @param {RuleContext} context */
40+
create(context) {
41+
/**
42+
* @param {ComponentTypeProp} prop
43+
* @param {Token[]} tokens
44+
* */
45+
const findKeyToken = (prop, tokens) =>
46+
tokens.find((token) => {
47+
const isKeyIdentifierEqual =
48+
prop.key.type === 'Identifier' && token.value === prop.key.name
49+
const isKeyLiteralEqual =
50+
prop.key.type === 'Literal' && token.value === prop.key.raw
51+
return isKeyIdentifierEqual || isKeyLiteralEqual
52+
})
53+
54+
return utils.defineScriptSetupVisitor(context, {
55+
onDefinePropsEnter(node, props) {
56+
if (!utils.hasWithDefaults(node)) {
57+
return
58+
}
59+
const withDefaultsProps = Object.keys(
60+
utils.getWithDefaultsPropExpressions(node)
61+
)
62+
const requiredProps = props.flatMap((item) =>
63+
item.type === 'type' && item.required ? [item] : []
64+
)
65+
66+
for (const prop of requiredProps) {
67+
if (withDefaultsProps.includes(prop.propName)) {
68+
const firstToken = context.getSourceCode().getFirstToken(prop.node)
69+
// skip setter & getter case
70+
if (firstToken.value === 'get' || firstToken.value === 'set') {
71+
return
72+
}
73+
// skip computed
74+
if (prop.node.computed) {
75+
return
76+
}
77+
const keyToken = findKeyToken(
78+
prop,
79+
context.getSourceCode().getTokens(prop.node)
80+
)
81+
if (!keyToken) return
82+
context.report({
83+
node: prop.node,
84+
loc: prop.node.loc,
85+
data: {
86+
key: prop.propName
87+
},
88+
message: `Prop "{{ key }}" should be optional.`,
89+
fix: (fixer) => fixer.insertTextAfter(keyToken, '?')
90+
})
91+
}
92+
}
93+
}
94+
})
95+
}
96+
}

0 commit comments

Comments
 (0)