diff --git a/docs/rules/prefer-true-attribute-shorthand.md b/docs/rules/prefer-true-attribute-shorthand.md index 2a70b92eb..700921ce1 100644 --- a/docs/rules/prefer-true-attribute-shorthand.md +++ b/docs/rules/prefer-true-attribute-shorthand.md @@ -81,12 +81,18 @@ Default options is `"always"`. ```json { - "vue/prefer-true-attribute-shorthand": ["error", "always" | "never"] + "vue/prefer-true-attribute-shorthand": ["error", + "always" | "never", + { + except: [] + } + ] } ``` - `"always"` (default) ... requires shorthand form. - `"never"` ... requires long form. +- `except` (`string[]`) ... specifies a list of attribute names that should be treated differently. ### `"never"` @@ -105,6 +111,26 @@ Default options is `"always"`. +### `"never", { 'except': ['value', '/^foo-/'] }` + + + +```vue + +``` + + + ## :couple: Related Rules - [vue/no-boolean-default](./no-boolean-default.md) diff --git a/lib/rules/prefer-true-attribute-shorthand.js b/lib/rules/prefer-true-attribute-shorthand.js index 42d3326a9..817525d1d 100644 --- a/lib/rules/prefer-true-attribute-shorthand.js +++ b/lib/rules/prefer-true-attribute-shorthand.js @@ -4,8 +4,60 @@ */ 'use strict' +const { toRegExp } = require('../utils/regexp') const utils = require('../utils') +/** + * @typedef { 'always' | 'never' } PreferOption + */ + +/** + * @param {VDirective | VAttribute} node + * @returns {string | null} + */ +function getAttributeName(node) { + if (!node.directive) { + return node.key.rawName + } + + if ( + (node.key.name.name === 'bind' || node.key.name.name === 'model') && + node.key.argument && + node.key.argument.type === 'VIdentifier' + ) { + return node.key.argument.rawName + } + + return null +} +/** + * @param {VAttribute | VDirective} node + * @param {boolean} isExcepted + * @param {PreferOption} option + */ +function shouldConvertToLongForm(node, isExcepted, option) { + return ( + !node.directive && + !node.value && + (option === 'always' ? isExcepted : !isExcepted) + ) +} + +/** + * @param {VAttribute | VDirective} node + * @param {boolean} isExcepted + * @param {PreferOption} option + */ +function shouldConvertToShortForm(node, isExcepted, option) { + const isLiteralTrue = + node.directive && + node.value?.expression?.type === 'Literal' && + node.value.expression.value === true && + Boolean(node.key.argument) + + return isLiteralTrue && (option === 'always' ? !isExcepted : isExcepted) +} + module.exports = { meta: { type: 'suggestion', @@ -17,7 +69,20 @@ module.exports = { }, fixable: null, hasSuggestions: true, - schema: [{ enum: ['always', 'never'] }], + schema: [ + { enum: ['always', 'never'] }, + { + type: 'object', + properties: { + except: { + type: 'array', + items: { type: 'string' }, + uniqueItems: true + } + }, + additionalProperties: false + } + ], messages: { expectShort: "Boolean prop with 'true' value should be written in shorthand form.", @@ -34,68 +99,81 @@ module.exports = { create(context) { /** @type {'always' | 'never'} */ const option = context.options[0] || 'always' + /** @type {RegExp[]} */ + const exceptReg = (context.options[1]?.except || []).map(toRegExp) + + /** + * @param {VAttribute | VDirective} node + * @param {string} messageId + * @param {string} longVuePropText + * @param {string} longHtmlAttrText + */ + function reportLongForm( + node, + messageId, + longVuePropText, + longHtmlAttrText + ) { + context.report({ + node, + messageId, + suggest: [ + { + messageId: 'rewriteIntoLongVueProp', + fix: (fixer) => fixer.replaceText(node, longVuePropText) + }, + { + messageId: 'rewriteIntoLongHtmlAttr', + fix: (fixer) => fixer.replaceText(node, longHtmlAttrText) + } + ] + }) + } + + /** + * @param {VAttribute | VDirective} node + * @param {string} messageId + * @param {string} shortFormText + */ + function reportShortForm(node, messageId, shortFormText) { + context.report({ + node, + messageId, + suggest: [ + { + messageId: 'rewriteIntoShort', + fix: (fixer) => fixer.replaceText(node, shortFormText) + } + ] + }) + } 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 (!utils.isCustomComponent(node.parent.parent)) return - if (option !== 'always') { - return - } + const name = getAttributeName(node) + if (name === null) return - if ( - !node.directive || - !node.value || - !node.value.expression || - node.value.expression.type !== 'Literal' || - node.value.expression.value !== true - ) { - return - } + const isExcepted = exceptReg.some((re) => re.test(name)) - const { argument } = node.key - if (!argument) { - return + if (shouldConvertToLongForm(node, isExcepted, option)) { + const key = /** @type {VIdentifier} */ (node.key) + reportLongForm( + node, + 'expectLong', + `:${key.rawName}="true"`, + `${key.rawName}="${key.rawName}"` + ) + } else if (shouldConvertToShortForm(node, isExcepted, option)) { + const directiveKey = /** @type {VDirectiveKey} */ (node.key) + if ( + directiveKey.argument && + directiveKey.argument.type === 'VIdentifier' + ) { + reportShortForm(node, 'expectShort', directiveKey.argument.rawName) + } } - - 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 index 43253fd69..80965bac6 100644 --- a/tests/lib/rules/prefer-true-attribute-shorthand.js +++ b/tests/lib/rules/prefer-true-attribute-shorthand.js @@ -148,6 +148,24 @@ tester.run('prefer-true-attribute-shorthand', rule, { `, options: ['never'] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['always', { except: ['value', '/^foo-/'] }] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['never', { except: ['value', '/^foo-/'] }] } ], invalid: [ @@ -280,6 +298,98 @@ tester.run('prefer-true-attribute-shorthand', rule, { ] } ] + }, + { + filename: 'test.vue', + code: ` + `, + output: null, + options: ['always', { except: ['value', '/^foo-/'] }], + errors: [ + { + messageId: 'expectLong', + line: 3, + column: 17, + suggestions: [ + { + messageId: 'rewriteIntoLongVueProp', + output: ` + ` + }, + { + messageId: 'rewriteIntoLongHtmlAttr', + output: ` + ` + } + ] + }, + { + messageId: 'expectLong', + line: 3, + column: 23, + suggestions: [ + { + messageId: 'rewriteIntoLongVueProp', + output: ` + ` + }, + { + messageId: 'rewriteIntoLongHtmlAttr', + output: ` + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + `, + output: null, + options: ['never', { except: ['value', '/^foo-/'] }], + errors: [ + { + messageId: 'expectShort', + line: 3, + column: 17, + suggestions: [ + { + messageId: 'rewriteIntoShort', + output: ` + ` + } + ] + }, + { + messageId: 'expectShort', + line: 3, + column: 31, + suggestions: [ + { + messageId: 'rewriteIntoShort', + output: ` + ` + } + ] + } + ] } ] })