Skip to content

Commit

Permalink
feat: add no-required-prop-with-default rule (#1943)
Browse files Browse the repository at this point in the history
* feat: add optional-props-using-with-defaults

* feat: improve rule name

* feat: change to problem

* feat: fix comments

* feat: add suggest to rule
  • Loading branch information
neferqiqi authored Sep 20, 2022
1 parent e9964e1 commit a4e807c
Show file tree
Hide file tree
Showing 5 changed files with 1,175 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ For example:
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | | :hammer: |
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
| [vue/no-ref-object-destructure](./no-ref-object-destructure.md) | disallow destructuring of ref objects that can lead to loss of reactivity | | :warning: |
| [vue/no-required-prop-with-default](./no-required-prop-with-default.md) | enforce props with default values ​​to be optional | :wrench::bulb: | :warning: |
| [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block | | :hammer: |
| [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods | | :hammer: |
| [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components | | :warning: |
Expand Down
96 changes: 96 additions & 0 deletions docs/rules/no-required-prop-with-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-required-prop-with-default
description: enforce props with default values ​​to be optional
---
# vue/no-required-prop-with-default

> 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.
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

## :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/no-required-prop-with-default': ['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/no-required-prop-with-default': ['error', { autoFix: true }]}">

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

</eslint-code-block>

## :wrench: Options

```json
{
"vue/no-required-prop-with-default": ["error", {
"autofix": false,
}]
}
```

- `"autofix"` ... If `true`, enable autofix. (Default: `false`)

## :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/no-required-prop-with-default.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-required-prop-with-default.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ module.exports = {
'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
'no-ref-object-destructure': require('./rules/no-ref-object-destructure'),
'no-required-prop-with-default': require('./rules/no-required-prop-with-default'),
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
'no-reserved-props': require('./rules/no-reserved-props'),
Expand Down
155 changes: 155 additions & 0 deletions lib/rules/no-required-prop-with-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* @author @neferqiqi
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

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

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

module.exports = {
meta: {
hasSuggestions: true,
type: 'problem',
docs: {
description: 'enforce props with default values ​​to be optional',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-required-prop-with-default.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
autofix: {
type: 'boolean'
}
},
additionalProperties: false
}
],
messages: {
requireOptional: `Prop "{{ key }}" should be optional.`,
fixRequiredProp: `Change this prop to be optional.`
}
},
/** @param {RuleContext} context */
create(context) {
let canAutoFix = false
const option = context.options[0]
if (option) {
canAutoFix = option.autofix
}

/**
* @param {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp | ComponentProp} prop
* */
const handleObjectProp = (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
},
messageId: 'requireOptional',
fix: canAutoFix
? (fixer) => fixer.replaceText(requiredNode, 'false')
: null,
suggest: canAutoFix
? null
: [
{
messageId: 'fixRequiredProp',
fix: (fixer) => fixer.replaceText(requiredNode, 'false')
}
]
})
}
}
}

return utils.compositingVisitors(
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
utils.getComponentPropsFromOptions(node).map(handleObjectProp)
}
}),
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(node, props) {
if (!utils.hasWithDefaults(node)) {
props.map(handleObjectProp)
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)) {
// skip setter & getter case
if (
prop.node.type === 'TSMethodSignature' &&
(prop.node.kind === 'get' || prop.node.kind === 'set')
) {
return
}
// skip computed
if (prop.node.computed) {
return
}
context.report({
node: prop.node,
loc: prop.node.loc,
data: {
key: prop.propName
},
messageId: 'requireOptional',
fix: canAutoFix
? (fixer) => fixer.insertTextAfter(prop.key, '?')
: null,
suggest: canAutoFix
? null
: [
{
messageId: 'fixRequiredProp',
fix: (fixer) => fixer.insertTextAfter(prop.key, '?')
}
]
})
}
}
}
})
)
}
}
Loading

0 comments on commit a4e807c

Please sign in to comment.