diff --git a/docs/rules/define-props-destructuring.md b/docs/rules/define-props-destructuring.md new file mode 100644 index 000000000..7f3bc7849 --- /dev/null +++ b/docs/rules/define-props-destructuring.md @@ -0,0 +1,95 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/define-props-destructuring +description: enforce consistent style for props destructuring +--- + +# vue/define-props-destructuring + +> enforce consistent style for props destructuring + +- :exclamation: _**This rule has not been released yet.**_ + +## :book: Rule Details + +This rule enforces a consistent style for handling Vue 3 Composition API props, allowing you to choose between requiring destructuring or prohibiting it. + +By default, the rule requires you to use destructuring syntax when using `defineProps` instead of storing props in a variable and warns against combining `withDefaults` with destructuring. + + + +```vue + +``` + + + +The rule applies to both JavaScript and TypeScript props: + + + +```vue + +``` + + + +## :wrench: Options + +```js +{ + "vue/define-props-destructuring": ["error", { + "destructure": "always" | "never" + }] +} +``` + +- `destructure` - Sets the destructuring preference for props + - `"always"` (default) - Requires destructuring when using `defineProps` and warns against using `withDefaults` with destructuring + - `"never"` - Requires using a variable to store props and prohibits destructuring + +### `"destructure": "never"` + + + +```vue + +``` + + + +## :books: Further Reading + +- [Reactive Props Destructure](https://vuejs.org/guide/components/props.html#reactive-props-destructure) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-props-destructuring.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-props-destructuring.js) diff --git a/docs/rules/index.md b/docs/rules/index.md index 55c5b96c9..7828b58bb 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -218,6 +218,7 @@ For example: | [vue/define-emits-declaration] | enforce declaration style of `defineEmits` | | :hammer: | | [vue/define-macros-order] | enforce order of compiler macros (`defineProps`, `defineEmits`, etc.) | :wrench::bulb: | :lipstick: | | [vue/define-props-declaration] | enforce declaration style of `defineProps` | | :hammer: | +| [vue/define-props-destructuring] | enforce consistent style for props destructuring | | :hammer: | | [vue/enforce-style-attribute] | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags | | :hammer: | | [vue/html-button-has-type] | disallow usage of button without an explicit type attribute | | :hammer: | | [vue/html-comment-content-newline] | enforce unified line break in HTML comments | :wrench: | :lipstick: | @@ -398,6 +399,7 @@ The following rules extend the rules provided by ESLint itself and apply them to [vue/define-emits-declaration]: ./define-emits-declaration.md [vue/define-macros-order]: ./define-macros-order.md [vue/define-props-declaration]: ./define-props-declaration.md +[vue/define-props-destructuring]: ./define-props-destructuring.md [vue/dot-location]: ./dot-location.md [vue/dot-notation]: ./dot-notation.md [vue/enforce-style-attribute]: ./enforce-style-attribute.md diff --git a/lib/index.js b/lib/index.js index 834e5f28b..e511536fa 100644 --- a/lib/index.js +++ b/lib/index.js @@ -58,6 +58,7 @@ const plugin = { 'define-emits-declaration': require('./rules/define-emits-declaration'), 'define-macros-order': require('./rules/define-macros-order'), 'define-props-declaration': require('./rules/define-props-declaration'), + 'define-props-destructuring': require('./rules/define-props-destructuring'), 'dot-location': require('./rules/dot-location'), 'dot-notation': require('./rules/dot-notation'), 'enforce-style-attribute': require('./rules/enforce-style-attribute'), diff --git a/lib/rules/define-props-destructuring.js b/lib/rules/define-props-destructuring.js new file mode 100644 index 000000000..65ec1dcd7 --- /dev/null +++ b/lib/rules/define-props-destructuring.js @@ -0,0 +1,79 @@ +/** + * @author Wayne Zhang + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce consistent style for props destructuring', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/define-props-destructuring.html' + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + destructure: { + enum: ['always', 'never'] + } + }, + additionalProperties: false + } + ], + messages: { + preferDestructuring: 'Prefer destructuring from `defineProps` directly.', + avoidDestructuring: 'Avoid destructuring from `defineProps`.', + avoidWithDefaults: 'Avoid using `withDefaults` with destructuring.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const options = context.options[0] || {} + const destructurePreference = options.destructure || 'always' + + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(node, props) { + const hasNoArgs = props.filter((prop) => prop.propName).length === 0 + if (hasNoArgs) { + return + } + + const hasDestructure = utils.isUsingPropsDestructure(node) + const hasWithDefaults = utils.hasWithDefaults(node) + + if (destructurePreference === 'never') { + if (hasDestructure) { + context.report({ + node, + messageId: 'avoidDestructuring' + }) + } + return + } + + if (!hasDestructure) { + context.report({ + node, + messageId: 'preferDestructuring' + }) + return + } + + if (hasWithDefaults) { + context.report({ + node: node.parent.callee, + messageId: 'avoidWithDefaults' + }) + } + } + }) + ) + } +} diff --git a/tests/lib/rules/define-props-destructuring.js b/tests/lib/rules/define-props-destructuring.js new file mode 100644 index 000000000..ec24b4328 --- /dev/null +++ b/tests/lib/rules/define-props-destructuring.js @@ -0,0 +1,212 @@ +/** + * @author Wayne Zhang + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/define-props-destructuring') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2015, + sourceType: 'module' + } +}) + +tester.run('define-props-destructuring', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + } + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + } + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'preferDestructuring', + line: 3, + column: 21 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'preferDestructuring', + line: 3, + column: 34 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'avoidWithDefaults', + line: 3, + column: 23 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + messageId: 'preferDestructuring', + line: 3, + column: 34 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + messageId: 'avoidWithDefaults', + line: 3, + column: 23 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + errors: [ + { + messageId: 'avoidDestructuring', + line: 3, + column: 23 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + errors: [ + { + messageId: 'avoidDestructuring', + line: 3, + column: 36 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + messageId: 'avoidDestructuring', + line: 3, + column: 23 + } + ] + } + ] +})