Skip to content

Commit

Permalink
feat: add optional-props-using-with-defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
neferqiqi committed Sep 13, 2022
1 parent f358817 commit 2c19140
Show file tree
Hide file tree
Showing 5 changed files with 1,130 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -248,6 +248,7 @@ For example:
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | :hammer: |
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | :hammer: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
| [vue/prefer-optional-props-using-with-defaults](./prefer-optional-props-using-with-defaults.md) | enforce props with default values ​​to be optional | :wrench: | :hammer: |
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
| [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: |
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
Expand Down
95 changes: 95 additions & 0 deletions docs/rules/prefer-optional-props-using-with-defaults.md
@@ -0,0 +1,95 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/prefer-optional-props-using-with-defaults
description: enforce props with default values ​​to be optional
---
# vue/prefer-optional-props-using-with-defaults

> enforce props with default values ​​to be optional
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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.

## :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.

<eslint-code-block fix :rules="{'vue/prefer-optional-props-using-with-defaults': ['error', { autoFix: true }]}">

```vue
<script setup lang="ts">
/* ✓ GOOD */
const props = withDefaults(
defineProps<{
name?: string | number
age?: number
}>(),
{
name: "Foo",
}
);
/* ✗ BAD */
const props = withDefaults(
defineProps<{
name: string | number
age?: number
}>(),
{
name: "Foo",
}
);
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/prefer-optional-props-using-with-defaults': ['error', { autoFix: true }]}">

```vue
<script setup lang="ts">
export default {
/* ✓ GOOD */
props: {
name: {
required: true,
default: 'Hello'
}
}
/* ✗ BAD */
props: {
name: {
required: true,
default: 'Hello'
}
}
}
</script>
```

</eslint-code-block>

## :wrench: Options

```json
{
"vue/prefer-optional-props-using-with-defaults": ["error", {
"autofix": false,
}]
}
```

- `"autofix"` ... If `true`, enable autofix.

## :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/prefer-optional-props-using-with-defaults.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-optional-props-using-with-defaults.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -157,6 +157,7 @@ module.exports = {
'order-in-components': require('./rules/order-in-components'),
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
'prefer-optional-props-using-with-defaults': require('./rules/prefer-optional-props-using-with-defaults'),
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
'prefer-template': require('./rules/prefer-template'),
Expand Down
154 changes: 154 additions & 0 deletions lib/rules/prefer-optional-props-using-with-defaults.js
@@ -0,0 +1,154 @@
/**
* @author @neferqiqi
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')
/**
* @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
*/

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

// ...

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce props with default values ​​to be optional',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/prefer-optional-props-using-with-defaults.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
autofix: {
type: 'boolean'
}
},
additionalProperties: false
}
],
messages: {
// ...
}
},
/** @param {RuleContext} context */
create(context) {
/**
* @param {ComponentTypeProp} prop
* @param {Token[]} tokens
* */
const findKeyToken = (prop, tokens) =>
tokens.find((token) => {
const isKeyIdentifierEqual =
prop.key.type === 'Identifier' && token.value === prop.key.name
const isKeyLiteralEqual =
prop.key.type === 'Literal' && token.value === prop.key.raw
return isKeyIdentifierEqual || isKeyLiteralEqual
})

let canAutoFix = false
const option = context.options[0]
if (option) {
canAutoFix = option.autofix
}

return utils.compositingVisitors(
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
utils.getComponentPropsFromOptions(node).map((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
},
message: `Prop "{{ key }}" should be optional.`,
fix: canAutoFix
? (fixer) => fixer.replaceText(requiredNode, 'false')
: null
})
}
}
})
}
}),
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(node, props) {
if (!utils.hasWithDefaults(node)) {
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)) {
const firstToken = context
.getSourceCode()
.getFirstToken(prop.node)
// skip setter & getter case
if (firstToken.value === 'get' || firstToken.value === 'set') {
return
}
// skip computed
if (prop.node.computed) {
return
}
const keyToken = findKeyToken(
prop,
context.getSourceCode().getTokens(prop.node)
)
if (!keyToken) return
context.report({
node: prop.node,
loc: prop.node.loc,
data: {
key: prop.propName
},
message: `Prop "{{ key }}" should be optional.`,
fix: canAutoFix
? (fixer) => fixer.insertTextAfter(keyToken, '?')
: null
})
}
}
}
})
)
}
}

0 comments on commit 2c19140

Please sign in to comment.