From eeece27139e79e3cfc9786dc19836f345b827b97 Mon Sep 17 00:00:00 2001 From: neferqiqi <724861805@qq.com> Date: Mon, 8 Aug 2022 15:46:12 +0800 Subject: [PATCH 1/5] feat: add optional-props-using-with-defaults --- docs/rules/README.md | 1 + ...efer-optional-props-using-with-defaults.md | 95 ++ lib/index.js | 1 + ...efer-optional-props-using-with-defaults.js | 154 +++ ...efer-optional-props-using-with-defaults.js | 879 ++++++++++++++++++ 5 files changed, 1130 insertions(+) create mode 100644 docs/rules/prefer-optional-props-using-with-defaults.md create mode 100644 lib/rules/prefer-optional-props-using-with-defaults.js create mode 100644 tests/lib/rules/prefer-optional-props-using-with-defaults.js diff --git a/docs/rules/README.md b/docs/rules/README.md index de00e7e53..951952b76 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -249,6 +249,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: | diff --git a/docs/rules/prefer-optional-props-using-with-defaults.md b/docs/rules/prefer-optional-props-using-with-defaults.md new file mode 100644 index 000000000..ce15b75b3 --- /dev/null +++ b/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: ***This rule has not been released yet.*** +- :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. + + + +```vue + +``` + + + + + +```vue + +``` + + + +## :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) diff --git a/lib/index.js b/lib/index.js index ca3a93084..43bfa74a8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -158,6 +158,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'), diff --git a/lib/rules/prefer-optional-props-using-with-defaults.js b/lib/rules/prefer-optional-props-using-with-defaults.js new file mode 100644 index 000000000..723b2cbc3 --- /dev/null +++ b/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 + }) + } + } + } + }) + ) + } +} diff --git a/tests/lib/rules/prefer-optional-props-using-with-defaults.js b/tests/lib/rules/prefer-optional-props-using-with-defaults.js new file mode 100644 index 000000000..a063422c2 --- /dev/null +++ b/tests/lib/rules/prefer-optional-props-using-with-defaults.js @@ -0,0 +1,879 @@ +/** + * @author neferqiqi + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/prefer-optional-props-using-with-defaults') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-optional-props-using-with-defaults', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + ` + }, + // ignore array prop + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + options: [{ autofix: true }], + output: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ autofix: true }], + output: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "na::me" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "na"me2" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "foo" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "foo" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ autofix: true }], + output: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "a" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: 'Prop "a" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + errors: [ + { + message: 'Prop "name" should be optional.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + errors: [ + { + message: 'Prop "name" should be optional.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + errors: [ + { + message: 'Prop "name" should be optional.', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + errors: [ + { + message: 'Prop "name" should be optional.', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 6 + } + ] + } + ] +}) From d90f391a45269da77667467b7f9124e9b8d815fe Mon Sep 17 00:00:00 2001 From: neferqiqi <724861805@qq.com> Date: Tue, 13 Sep 2022 23:33:30 +0800 Subject: [PATCH 2/5] feat: improve rule name --- docs/rules/README.md | 2 +- ...efaults.md => no-required-prop-with-default.md} | 14 +++++++------- lib/index.js | 2 +- ...efaults.js => no-required-prop-with-default.js} | 2 +- ...efaults.js => no-required-prop-with-default.js} | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) rename docs/rules/{prefer-optional-props-using-with-defaults.md => no-required-prop-with-default.md} (78%) rename lib/rules/{prefer-optional-props-using-with-defaults.js => no-required-prop-with-default.js} (98%) rename tests/lib/rules/{prefer-optional-props-using-with-defaults.js => no-required-prop-with-default.js} (99%) diff --git a/docs/rules/README.md b/docs/rules/README.md index 951952b76..43b74e6d4 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -228,6 +228,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: | :hammer: | | [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: | @@ -249,7 +250,6 @@ 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: | diff --git a/docs/rules/prefer-optional-props-using-with-defaults.md b/docs/rules/no-required-prop-with-default.md similarity index 78% rename from docs/rules/prefer-optional-props-using-with-defaults.md rename to docs/rules/no-required-prop-with-default.md index ce15b75b3..a3739f590 100644 --- a/docs/rules/prefer-optional-props-using-with-defaults.md +++ b/docs/rules/no-required-prop-with-default.md @@ -1,10 +1,10 @@ --- pageClass: rule-details sidebarDepth: 0 -title: vue/prefer-optional-props-using-with-defaults +title: vue/no-required-prop-with-default description: enforce props with default values ​​to be optional --- -# vue/prefer-optional-props-using-with-defaults +# vue/no-required-prop-with-default > enforce props with default values ​​to be optional @@ -17,7 +17,7 @@ If a prop is declared with a default value, whether it is required or not, we ca 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. - + ```vue ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [ @@ -874,6 +887,36 @@ tester.run('no-required-prop-with-default', rule, { line: 6 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] } ] })