From a56c597d20f97e99a2d5d1fe187bc19324fc952e Mon Sep 17 00:00:00 2001 From: waynzh Date: Wed, 24 Jan 2024 00:31:43 +0800 Subject: [PATCH 1/3] feat: add v-bind-same-name-style rule --- lib/rules/v-bind-same-name-style.js | 118 +++++++++++++++++ tests/lib/rules/v-bind-same-name-style.js | 151 ++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 lib/rules/v-bind-same-name-style.js create mode 100644 tests/lib/rules/v-bind-same-name-style.js diff --git a/lib/rules/v-bind-same-name-style.js b/lib/rules/v-bind-same-name-style.js new file mode 100644 index 000000000..614eb6163 --- /dev/null +++ b/lib/rules/v-bind-same-name-style.js @@ -0,0 +1,118 @@ +/** + * @author waynzh + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') +const casing = require('../utils/casing') + +/** + * @typedef { VDirectiveKey & { name: VIdentifier & { name: 'bind' }, argument: VExpressionContainer | VIdentifier } } VBindDirectiveKey + * @typedef { VDirective & { key: VBindDirectiveKey } } VBindDirective + */ + +/** + * @param {VBindDirective} node + * @returns {string | null} + */ +function getAttributeName(node) { + // not support VExpressionContainer e.g. :[attribute] + if (node.key.argument.type === 'VIdentifier') { + return node.key.argument.rawName + } + + return null +} + +/** + * @param {VBindDirective} node + * @returns {string | null} + */ +function getValueName(node) { + if (node.value?.expression?.type === 'Identifier') { + return node.value.expression.name + } + + return null +} + +/** + * @param {VBindDirective} node + * @returns {boolean} + */ +function isSameName(node) { + const attrName = getAttributeName(node) + const valueName = getValueName(node) + return Boolean(attrName && valueName && attrName === valueName) +} + +/** + * @param {VBindDirectiveKey} key + * @returns {number} + */ +function getCutStart(key) { + const modifiers = key.modifiers + return modifiers.length > 0 + ? modifiers[modifiers.length - 1].range[1] + : key.argument.range[1] +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce `v-bind` same name directive style', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/v-bind-same-name-style.html' + }, + fixable: 'code', + schema: [{ enum: ['always', 'never'] }], + messages: { + expectedShorthand: 'Expected shorthand same name.', + unexpectedShorthand: 'Unexpected shorthand same name.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const preferShorthand = context.options[0] === 'always' + + return utils.defineTemplateBodyVisitor(context, { + /** @param {VBindDirective} node */ + "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"( + node + ) { + if (!isSameName(node)) return + + const isShortHand = utils.isVBindSameNameShorthand(node) + if (isShortHand === preferShorthand) { + return + } + + let messageId = 'unexpectedShorthand' + if (preferShorthand) { + messageId = 'expectedShorthand' + } + + context.report({ + node, + loc: node.loc, + messageId, + *fix(fixer) { + if (preferShorthand && node.value) { + /** @type {Range} */ + const valueRange = [getCutStart(node.key), node.range[1]] + + yield fixer.removeRange(valueRange) + } else if (node.key.argument.type === 'VIdentifier') { + yield fixer.insertTextAfter( + node, + `="${casing.camelCase(node.key.argument.rawName)}"` + ) + } + } + }) + } + }) + } +} diff --git a/tests/lib/rules/v-bind-same-name-style.js b/tests/lib/rules/v-bind-same-name-style.js new file mode 100644 index 000000000..9c71e91f8 --- /dev/null +++ b/tests/lib/rules/v-bind-same-name-style.js @@ -0,0 +1,151 @@ +/** + * @author waynzh + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/v-bind-same-name-style') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +const expectedShorthand = 'Expected shorthand same name.' +const unexpectedShorthand = 'Unexpected shorthand same name.' + +tester.run('v-bind-same-name-style', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + // never + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '', + options: ['never'] + }, + { + // modifier + filename: 'test.vue', + code: '', + options: ['never'] + }, + { + filename: 'test.vue', + code: '', + options: ['never'] + }, + // always + { + filename: 'test.vue', + code: '', + options: ['always'] + }, + { + filename: 'test.vue', + code: '', + options: ['always'] + }, + { + filename: 'test.vue', + code: '', + options: ['always'] + }, + { + // modifier + filename: 'test.vue', + code: '', + options: ['always'] + }, + { + filename: 'test.vue', + code: '', + options: ['always'] + } + ], + invalid: [ + // never + { + filename: 'test.vue', + code: '', + output: '', + errors: [unexpectedShorthand] + }, + { + filename: 'test.vue', + code: '', + output: '', + errors: [unexpectedShorthand] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['never'], + errors: [unexpectedShorthand] + }, + { + // modifier + filename: 'test.vue', + code: '', + output: '', + options: ['never'], + errors: [unexpectedShorthand] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['never'], + errors: [unexpectedShorthand] + }, + // always + { + filename: 'test.vue', + code: '', + output: '', + options: ['always'], + errors: [expectedShorthand] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['always'], + errors: [expectedShorthand] + }, + { + // modifier + filename: 'test.vue', + code: '', + output: '', + options: ['always'], + errors: [expectedShorthand] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['always'], + errors: [expectedShorthand] + } + ] +}) From 8c2dce7c1df3aae5de50481bda6f0f68b288e873 Mon Sep 17 00:00:00 2001 From: waynzh Date: Wed, 24 Jan 2024 00:35:00 +0800 Subject: [PATCH 2/3] docs: add docs --- docs/rules/index.md | 2 + docs/rules/no-restricted-static-attribute.md | 1 - docs/rules/no-restricted-v-bind.md | 1 - docs/rules/require-explicit-slots.md | 8 ++- docs/rules/v-bind-same-name-style.md | 68 ++++++++++++++++++++ lib/index.js | 2 + 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 docs/rules/v-bind-same-name-style.md diff --git a/docs/rules/index.md b/docs/rules/index.md index 1367f797e..039099f35 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -269,6 +269,7 @@ For example: | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | :hammer: | | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: | +| [vue/require-explicit-slots](./require-explicit-slots.md) | require slots to be explicitly defined | | :warning: | | [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | :hammer: | | [vue/require-macro-variable-name](./require-macro-variable-name.md) | require a certain macro variable name | :bulb: | :hammer: | | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | :bulb: | :hammer: | @@ -278,6 +279,7 @@ For example: | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `