/** * @author Toru Nagashima * @copyright 2017 Toru Nagashima. All rights reserved. * 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 {string} name * @returns {string} */ function kebabCaseToCamelCase(name) { return casing.isKebabCase(name) ? casing.camelCase(name) : name } /** * @param {VBindDirective} node * @returns {boolean} */ function isSameName(node) { const attrName = node.key.argument.type === 'VIdentifier' ? node.key.argument.rawName : null const valueName = node.value?.expression?.type === 'Identifier' ? node.value.expression.name : null if (!attrName || !valueName) return false return kebabCaseToCamelCase(attrName) === kebabCaseToCamelCase(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` directive style', categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'], url: 'https://eslint.vuejs.org/rules/v-bind-style.html' }, fixable: 'code', schema: [ { enum: ['shorthand', 'longform'] }, { type: 'object', properties: { sameNameShorthand: { enum: ['always', 'never', 'ignore'] } }, additionalProperties: false } ], messages: { expectedLonghand: "Expected 'v-bind' before ':'.", unexpectedLonghand: "Unexpected 'v-bind' before ':'.", expectedLonghandForProp: "Expected 'v-bind:' instead of '.'.", expectedShorthand: 'Expected same-name shorthand.', unexpectedShorthand: 'Unexpected same-name shorthand.' } }, /** @param {RuleContext} context */ create(context) { const preferShorthand = context.options[0] !== 'longform' /** @type {"always" | "never" | "ignore"} */ const sameNameShorthand = context.options[1]?.sameNameShorthand || 'ignore' /** @param {VBindDirective} node */ function checkAttributeStyle(node) { const shorthandProp = node.key.name.rawName === '.' const shorthand = node.key.name.rawName === ':' || shorthandProp if (shorthand === preferShorthand) { return } let messageId = 'expectedLonghand' if (preferShorthand) { messageId = 'unexpectedLonghand' } else if (shorthandProp) { messageId = 'expectedLonghandForProp' } context.report({ node, loc: node.loc, messageId, *fix(fixer) { if (preferShorthand) { yield fixer.remove(node.key.name) } else { yield fixer.insertTextBefore(node, 'v-bind') if (shorthandProp) { // Replace `.` by `:`. yield fixer.replaceText(node.key.name, ':') // Insert `.prop` modifier if it doesn't exist. const modifier = node.key.modifiers[0] const isAutoGeneratedPropModifier = modifier.name === 'prop' && modifier.rawName === '' if (isAutoGeneratedPropModifier) { yield fixer.insertTextBefore(modifier, '.prop') } } } } }) } /** @param {VBindDirective} node */ function checkAttributeSameName(node) { if (sameNameShorthand === 'ignore' || !isSameName(node)) return const preferShorthand = sameNameShorthand === 'always' const isShorthand = utils.isVBindSameNameShorthand(node) if (isShorthand === preferShorthand) { return } const messageId = preferShorthand ? 'expectedShorthand' : 'unexpectedShorthand' context.report({ node, loc: node.loc, messageId, *fix(fixer) { if (preferShorthand) { /** @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, `="${kebabCaseToCamelCase(node.key.argument.rawName)}"` ) } } }) } return utils.defineTemplateBodyVisitor(context, { /** @param {VBindDirective} node */ "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"( node ) { checkAttributeSameName(node) checkAttributeStyle(node) } }) } }