diff --git a/docs/rules/README.md b/docs/rules/README.md index b74075445..b2f19b512 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -353,6 +353,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-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 | | | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | | [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | diff --git a/docs/rules/prefer-true-attribute-shorthand.md b/docs/rules/prefer-true-attribute-shorthand.md new file mode 100644 index 000000000..b2b521b6a --- /dev/null +++ b/docs/rules/prefer-true-attribute-shorthand.md @@ -0,0 +1,110 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-true-attribute-shorthand +description: require shorthand form attribute when `v-bind` value is `true` +--- +# vue/prefer-true-attribute-shorthand + +> require shorthand form attribute when `v-bind` value is `true` + +- :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 + +`v-bind` attribute with `true` value usually can be written in shorthand form. This can reduce verbosity. + + + +```vue + +``` + + + +::: warning Warning +The shorthand form is not always equivalent! If a prop accepts multiple types, but Boolean is not the first one, a shorthand prop won't pass `true`. +::: + +```vue + +``` + +**Shorthand form:** + +```vue + +``` + +``` +bool: true (boolean) +boolOrString: true (boolean) +stringOrBool: "" (string) +``` + +**Longhand form:** + +```vue + +``` + +``` +bool: true (boolean) +boolOrString: true (boolean) +stringOrBool: true (boolean) +``` + +Those two calls will introduce different render result. See [this demo](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBNeUNvbXBvbmVudCBmcm9tICcuL015Q29tcG9uZW50LnZ1ZSdcbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIFNob3J0aGFuZCBmb3JtOlxuICA8TXlDb21wb25lbnQgYm9vbCBib29sLW9yLXN0cmluZyBzdHJpbmctb3ItYm9vbCAvPlxuICBcbiAgTG9uZ2hhbmQgZm9ybTpcbiAgPE15Q29tcG9uZW50IDpib29sPVwidHJ1ZVwiIDpib29sLW9yLXN0cmluZz1cInRydWVcIiA6c3RyaW5nLW9yLWJvb2w9XCJ0cnVlXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIk15Q29tcG9uZW50LnZ1ZSI6IjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIHByb3BzOiB7XG4gICAgYm9vbDogQm9vbGVhbixcbiAgICBib29sT3JTdHJpbmc6IFtCb29sZWFuLCBTdHJpbmddLFxuICAgIHN0cmluZ09yQm9vbDogW1N0cmluZywgQm9vbGVhbl0sXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxwcmU+XG5ib29sOiB7e2Jvb2x9fSAoe3sgdHlwZW9mIGJvb2wgfX0pXG5ib29sT3JTdHJpbmc6IHt7Ym9vbE9yU3RyaW5nfX0gKHt7IHR5cGVvZiBib29sT3JTdHJpbmcgfX0pXG5zdHJpbmdPckJvb2w6IHt7c3RyaW5nT3JCb29sfX0gKHt7IHR5cGVvZiBzdHJpbmdPckJvb2wgfX0pXG4gIDwvcHJlPlxuPC90ZW1wbGF0ZT4ifQ==). + +## :wrench: Options + +Default options is `"always"`. + +```json +{ + "vue/prefer-true-attribute-shorthand": ["error", "always" | "never"] +} +``` + +- `"always"` (default) ... requires shorthand form. +- `"never"` ... requires long form. + +### `"never"` + + + +```vue + +``` + + + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-true-attribute-shorthand.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-true-attribute-shorthand.js) diff --git a/lib/index.js b/lib/index.js index 5d770e8b2..941de0b56 100644 --- a/lib/index.js +++ b/lib/index.js @@ -159,6 +159,7 @@ module.exports = { 'padding-line-between-blocks': require('./rules/padding-line-between-blocks'), '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'), 'prop-name-casing': require('./rules/prop-name-casing'), 'quote-props': require('./rules/quote-props'), 'require-component-is': require('./rules/require-component-is'), diff --git a/lib/rules/prefer-true-attribute-shorthand.js b/lib/rules/prefer-true-attribute-shorthand.js new file mode 100644 index 000000000..00fe95489 --- /dev/null +++ b/lib/rules/prefer-true-attribute-shorthand.js @@ -0,0 +1,110 @@ +/** + * @author Pig Fang + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'require shorthand form attribute when `v-bind` value is `true`', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/prefer-true-attribute-shorthand.html' + }, + fixable: null, + hasSuggestions: true, + schema: [{ enum: ['always', 'never'] }], + messages: { + expectShort: + "Boolean prop with 'true' value should be written in shorthand form.", + expectLong: + "Boolean prop with 'true' value should be written in long form.", + rewriteIntoShort: 'Rewrite this prop into shorthand form.', + rewriteIntoLongVueProp: + 'Rewrite this prop into long-form Vue component prop.', + rewriteIntoLongHtmlAttr: + 'Rewrite this prop into long-form HTML attribute.' + } + }, + /** @param {RuleContext} context */ + create(context) { + /** @type {'always' | 'never'} */ + const option = context.options[0] || 'always' + + return utils.defineTemplateBodyVisitor(context, { + VAttribute(node) { + if (!utils.isCustomComponent(node.parent.parent)) { + return + } + + if (option === 'never' && !node.directive && !node.value) { + context.report({ + node, + messageId: 'expectLong', + suggest: [ + { + messageId: 'rewriteIntoLongVueProp', + fix: (fixer) => + fixer.replaceText(node, `:${node.key.rawName}="true"`) + }, + { + messageId: 'rewriteIntoLongHtmlAttr', + fix: (fixer) => + fixer.replaceText( + node, + `${node.key.rawName}="${node.key.rawName}"` + ) + } + ] + }) + return + } + + if (option !== 'always') { + return + } + + if ( + !node.directive || + !node.value || + !node.value.expression || + node.value.expression.type !== 'Literal' || + node.value.expression.value !== true + ) { + return + } + + const { argument } = node.key + if (!argument) { + return + } + + context.report({ + node, + messageId: 'expectShort', + suggest: [ + { + messageId: 'rewriteIntoShort', + fix: (fixer) => { + const sourceCode = context.getSourceCode() + return fixer.replaceText(node, sourceCode.getText(argument)) + } + } + ] + }) + } + }) + } +} diff --git a/tests/lib/rules/prefer-true-attribute-shorthand.js b/tests/lib/rules/prefer-true-attribute-shorthand.js new file mode 100644 index 000000000..e7d52ef7f --- /dev/null +++ b/tests/lib/rules/prefer-true-attribute-shorthand.js @@ -0,0 +1,283 @@ +/** + * @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-true-attribute-shorthand') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-true-attribute-shorthand', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['always'] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['never'] + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + `, + errors: [ + { + messageId: 'expectShort', + line: 3, + column: 17, + suggestions: [ + { + messageId: 'rewriteIntoShort', + output: ` + ` + } + ] + } + ], + output: null + }, + { + filename: 'test.vue', + code: ` + `, + errors: [ + { + messageId: 'expectShort', + line: 3, + column: 17, + suggestions: [ + { + messageId: 'rewriteIntoShort', + output: ` + ` + } + ] + } + ], + output: null + }, + { + filename: 'test.vue', + code: ` + `, + options: ['always'], + errors: [ + { + messageId: 'expectShort', + line: 3, + column: 17, + suggestions: [ + { + messageId: 'rewriteIntoShort', + output: ` + ` + } + ] + } + ], + output: null + }, + { + filename: 'test.vue', + code: ` + `, + options: ['always'], + errors: [ + { + messageId: 'expectShort', + line: 3, + column: 17, + suggestions: [ + { + messageId: 'rewriteIntoShort', + output: ` + ` + } + ] + } + ], + output: null + }, + { + filename: 'test.vue', + code: ` + `, + options: ['never'], + errors: [ + { + messageId: 'expectLong', + line: 3, + column: 17, + suggestions: [ + { + output: ` + ` + }, + { + output: ` + ` + } + ] + } + ], + output: null + } + ] +})