Skip to content

Commit a3dfc07

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

File tree

5 files changed

+818
-0
lines changed

5 files changed

+818
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ For example:
247247
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | :hammer: |
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: |
250+
| [vue/optional-props-using-with-defaults](./optional-props-using-with-defaults.md) | enforce props with default values ​​to be optional | :wrench: | :hammer: |
250251
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
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: |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/optional-props-using-with-defaults
5+
description: enforce props with default values ​​to be optional
6+
---
7+
# vue/optional-props-using-with-defaults
8+
9+
> enforce props with default values ​​to be optional
10+
11+
- :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.
12+
13+
## :book: Rule Details
14+
15+
This rule enforce props with default values ​​to be optional.
16+
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.
17+
18+
<eslint-code-block fix :rules="{'vue/optional-props-using-with-defaults': ['error']}">
19+
20+
```vue
21+
<script setup lang="ts">
22+
/* ✓ GOOD */
23+
const props = withDefaults(
24+
defineProps<{
25+
name?: string | number
26+
age?: number
27+
}>(),
28+
{
29+
name: "Foo",
30+
}
31+
);
32+
33+
/* ✗ BAD */
34+
const props = withDefaults(
35+
defineProps<{
36+
name: string | number
37+
age?: number
38+
}>(),
39+
{
40+
name: "Foo",
41+
}
42+
);
43+
</script>
44+
```
45+
46+
</eslint-code-block>
47+
48+
## :wrench: Options
49+
50+
Nothing.
51+
52+
## :mag: Implementation
53+
54+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/optional-props-using-with-defaults.js)
55+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/optional-props-using-with-defaults.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ module.exports = {
154154
'object-shorthand': require('./rules/object-shorthand'),
155155
'one-component-per-file': require('./rules/one-component-per-file'),
156156
'operator-linebreak': require('./rules/operator-linebreak'),
157+
'optional-props-using-with-defaults': require('./rules/optional-props-using-with-defaults'),
157158
'order-in-components': require('./rules/order-in-components'),
158159
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
159160
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
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/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)