diff --git a/docs/rules/README.md b/docs/rules/README.md index 6e879b2f6..4a46ba66c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -352,6 +352,7 @@ For example: | [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | | [vue/prefer-import-from-vue](./prefer-import-from-vue.md) | enforce import from 'vue' instead of import from '@vue/*' | :wrench: | +| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | diff --git a/docs/rules/prefer-prop-type-boolean-first.md b/docs/rules/prefer-prop-type-boolean-first.md new file mode 100644 index 000000000..3bf2edb4a --- /dev/null +++ b/docs/rules/prefer-prop-type-boolean-first.md @@ -0,0 +1,60 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-prop-type-boolean-first +description: enforce `Boolean` comes first in component prop types +--- +# vue/prefer-prop-type-boolean-first + +> enforce `Boolean` comes first in component prop types + +- :exclamation: ***This rule has not been released yet.*** +- :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 + +When declaring types of a property in component, we can use array style to accept multiple types. + +When using components in template, +we can use shorthand-style property if its value is `true`. + +However, if a property allows `Boolean` or `String` and we use it with shorthand form in somewhere else, +different types order can introduce different behaviors: +If `Boolean` comes first, it will be `true`; if `String` comes first, it will be `""` (empty string). + +See [this demo](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBNeUNvbXBvbmVudCBmcm9tICcuL015Q29tcG9uZW50LnZ1ZSdcbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIFNob3J0aGFuZCBmb3JtOlxuICA8TXlDb21wb25lbnQgYm9vbCBib29sLW9yLXN0cmluZyBzdHJpbmctb3ItYm9vbCAvPlxuICBcbiAgTG9uZ2hhbmQgZm9ybTpcbiAgPE15Q29tcG9uZW50IDpib29sPVwidHJ1ZVwiIDpib29sLW9yLXN0cmluZz1cInRydWVcIiA6c3RyaW5nLW9yLWJvb2w9XCJ0cnVlXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIk15Q29tcG9uZW50LnZ1ZSI6IjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIHByb3BzOiB7XG4gICAgYm9vbDogQm9vbGVhbixcbiAgICBib29sT3JTdHJpbmc6IFtCb29sZWFuLCBTdHJpbmddLFxuICAgIHN0cmluZ09yQm9vbDogW1N0cmluZywgQm9vbGVhbl0sXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxwcmU+XG5ib29sOiB7e2Jvb2x9fSAoe3sgdHlwZW9mIGJvb2wgfX0pXG5ib29sT3JTdHJpbmc6IHt7Ym9vbE9yU3RyaW5nfX0gKHt7IHR5cGVvZiBib29sT3JTdHJpbmcgfX0pXG5zdHJpbmdPckJvb2w6IHt7c3RyaW5nT3JCb29sfX0gKHt7IHR5cGVvZiBzdHJpbmdPckJvb2wgfX0pXG4gIDwvcHJlPlxuPC90ZW1wbGF0ZT4ifQ==). + + + +```vue + +``` + + + +## :couple: Related Rules + +- [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-prop-type-boolean-first.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-prop-type-boolean-first.js) diff --git a/docs/rules/prefer-true-attribute-shorthand.md b/docs/rules/prefer-true-attribute-shorthand.md index f2704f4cd..c64f9a8c5 100644 --- a/docs/rules/prefer-true-attribute-shorthand.md +++ b/docs/rules/prefer-true-attribute-shorthand.md @@ -107,6 +107,7 @@ Default options is `"always"`. ## :couple: Related Rules - [vue/no-boolean-default](./no-boolean-default.md) +- [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) ## :rocket: Version diff --git a/lib/index.js b/lib/index.js index 97ff805d3..4e29c8ed0 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-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'), 'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'), diff --git a/lib/rules/prefer-prop-type-boolean-first.js b/lib/rules/prefer-prop-type-boolean-first.js new file mode 100644 index 000000000..215262d3b --- /dev/null +++ b/lib/rules/prefer-prop-type-boolean-first.js @@ -0,0 +1,115 @@ +/** + * @author Pig Fang + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * @param {ArrayExpression} node + * @param {RuleContext} context + */ +function checkArrayExpression(node, context) { + const booleanType = node.elements.find( + (element) => + element && element.type === 'Identifier' && element.name === 'Boolean' + ) + if (!booleanType) { + return + } + const booleanTypeIndex = node.elements.indexOf(booleanType) + if (booleanTypeIndex > 0) { + context.report({ + node: booleanType, + messageId: 'shouldBeFirst', + suggest: [ + { + messageId: 'moveToFirst', + fix: (fixer) => { + const sourceCode = context.getSourceCode() + + const elements = node.elements.slice() + elements.splice(booleanTypeIndex, 1) + const code = elements + .filter(utils.isDef) + .map((element) => sourceCode.getText(element)) + code.unshift('Boolean') + + return fixer.replaceText(node, `[${code.join(', ')}]`) + } + } + ] + }) + } +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'enforce `Boolean` comes first in component prop types', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/prefer-prop-type-boolean-first.html' + }, + fixable: null, + // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- `context.report` with suggestion is not recognized in `checkArrayExpression` + hasSuggestions: true, + schema: [], + messages: { + shouldBeFirst: 'Type `Boolean` should be at first in prop types.', + moveToFirst: 'Move `Boolean` to be first in prop types.' + } + }, + /** @param {RuleContext} context */ + create(context) { + /** + * @param {import('../utils').ComponentProp} prop + */ + function checkProperty(prop) { + const { value } = prop + if (!value) { + return + } + + if (value.type === 'ArrayExpression') { + checkArrayExpression(value, context) + } else if (value.type === 'ObjectExpression') { + const type = value.properties.find( + /** @return {property is Property} */ + (property) => + property.type === 'Property' && + utils.getStaticPropertyName(property) === 'type' + ) + if (!type || type.value.type !== 'ArrayExpression') { + return + } + checkArrayExpression(type.value, context) + } + } + + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(_, props) { + props.forEach(checkProperty) + } + }), + utils.executeOnVue(context, (obj) => { + const props = utils.getComponentPropsFromOptions(obj) + props.forEach(checkProperty) + }) + ) + } +} diff --git a/tests/lib/rules/prefer-prop-type-boolean-first.js b/tests/lib/rules/prefer-prop-type-boolean-first.js new file mode 100644 index 000000000..9f67e336b --- /dev/null +++ b/tests/lib/rules/prefer-prop-type-boolean-first.js @@ -0,0 +1,317 @@ +/** + * @author Pig Fang + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/prefer-prop-type-boolean-first') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-prop-type-boolean-first', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 5, + column: 29, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 5, + column: 37, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 5, + column: 29, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 5, + column: 37, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 4, + column: 27, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 4, + column: 35, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 4, + column: 35, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'shouldBeFirst', + line: 4, + column: 43, + suggestions: [ + { + messageId: 'moveToFirst', + output: ` + + ` + } + ] + } + ] + } + ] +})