diff --git a/docs/rules/README.md b/docs/rules/README.md index e44b2855b..860b0f731 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -287,6 +287,7 @@ For example: | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | | | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | | | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | | +| [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | | | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | | | [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | | diff --git a/docs/rules/no-restricted-v-bind.md b/docs/rules/no-restricted-v-bind.md new file mode 100644 index 000000000..cc141f1bd --- /dev/null +++ b/docs/rules/no-restricted-v-bind.md @@ -0,0 +1,119 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-restricted-v-bind +description: disallow specific argument in `v-bind` +--- +# vue/no-restricted-v-bind +> disallow specific argument in `v-bind` + +## :book: Rule Details + +This rule allows you to specify `v-bind` argument names that you don't want to use in your application. + +## :wrench: Options + +This rule takes a list of strings, where each string is a argument name or pattern to be restricted: + +```json +{ + "vue/no-restricted-v-bind": ["error", "/^v-/", "foo", "bar"] +} +``` + + + +```vue + +``` + + + +By default, `'/^v-/'` is set. This prevents mistakes intended to be directives. + + + +```vue + +``` + + + +Alternatively, the rule also accepts objects. + +```json +{ + "vue/no-restricted-v-bind": ["error", + { + "argument": "/^v-/", + "message": "Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive." + }, + { + "argument": "foo", + "message": "Use \"v-bind:x\" instead." + }, + { + "argument": "bar", + "message": "\":bar\" is deprecated." + } + ] +} +``` + +The following properties can be specified for the object. + +- `argument` ... Specify the argument name or pattern or `null`. If `null` is specified, it matches `v-bind=`. +- `modifiers` ... Specifies an array of the modifier names. If specified, it will only be reported if the specified modifier is used. +- `element` ... Specify the element name or pattern. If specified, it will only be reported if used on the specified element. +- `message` ... Specify an optional custom message. + +### `{ "argument": "foo", "modifiers": ["prop"] }` + + + +```vue + +``` + + + +### `{ "argument": "foo", "element": "MyButton" }` + + + +```vue + +``` + + + +## :couple: Related rules + +- [vue/no-restricted-static-attribute] + +[vue/no-restricted-static-attribute]: ./no-restricted-static-attribute.md + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-v-bind.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-v-bind.js) diff --git a/lib/index.js b/lib/index.js index 04b4af116..974ea15e0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -81,6 +81,7 @@ module.exports = { 'no-reserved-component-names': require('./rules/no-reserved-component-names'), 'no-reserved-keys': require('./rules/no-reserved-keys'), 'no-restricted-syntax': require('./rules/no-restricted-syntax'), + 'no-restricted-v-bind': require('./rules/no-restricted-v-bind'), 'no-setup-props-destructure': require('./rules/no-setup-props-destructure'), 'no-shared-component-data': require('./rules/no-shared-component-data'), 'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'), diff --git a/lib/rules/no-restricted-v-bind.js b/lib/rules/no-restricted-v-bind.js new file mode 100644 index 000000000..a87be4d2d --- /dev/null +++ b/lib/rules/no-restricted-v-bind.js @@ -0,0 +1,189 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +// @ts-check +'use strict' + +const utils = require('../utils') +const regexp = require('../utils/regexp') + +/** + * @typedef {import('vue-eslint-parser').AST.VDirectiveKey} VDirectiveKey + * @typedef {import('vue-eslint-parser').AST.VIdentifier} VIdentifier + */ +/** + * @typedef {object} ParsedOption + * @property { (key: VDirectiveKey) => boolean } test + * @property {string[]} modifiers + * @property {boolean} [useElement] + * @property {string} [message] + */ + +const DEFAULT_OPTIONS = [ + { + argument: '/^v-/', + message: + 'Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive.' + } +] + +/** + * @param {string} str + * @returns {(str: string) => boolean} + */ +function buildMatcher(str) { + if (regexp.isRegExp(str)) { + const re = regexp.toRegExp(str) + return (s) => { + re.lastIndex = 0 + return re.test(s) + } + } + return (s) => s === str +} +/** + * @param {any} option + * @returns {ParsedOption} + */ +function parseOption(option) { + if (typeof option === 'string') { + const matcher = buildMatcher(option) + return { + test(key) { + return ( + key.argument && + key.argument.type === 'VIdentifier' && + matcher(key.argument.rawName) + ) + }, + modifiers: [] + } + } + if (option === null) { + return { + test(key) { + return key.argument === null + }, + modifiers: [] + } + } + const parsed = parseOption(option.argument) + if (option.modifiers) { + const argTest = parsed.test + parsed.test = (key) => { + if (!argTest(key)) { + return false + } + return option.modifiers.every((modName) => { + return key.modifiers.some((mid) => mid.name === modName) + }) + } + parsed.modifiers = option.modifiers + } + if (option.element) { + const argTest = parsed.test + const tagMatcher = buildMatcher(option.element) + parsed.test = (key) => { + if (!argTest(key)) { + return false + } + const element = key.parent.parent.parent + return tagMatcher(element.rawName) + } + parsed.useElement = true + } + parsed.message = option.message + return parsed +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow specific argument in `v-bind`', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-restricted-v-bind.html' + }, + fixable: null, + schema: { + type: 'array', + items: { + oneOf: [ + { type: ['string', 'null'] }, + { + type: 'object', + properties: { + argument: { type: ['string', 'null'] }, + modifiers: { + type: 'array', + items: { + type: 'string', + enum: ['prop', 'camel', 'sync'] + }, + uniqueItems: true + }, + element: { type: 'string' }, + message: { type: 'string', minLength: 1 } + }, + required: ['argument'], + additionalProperties: false + } + ] + }, + uniqueItems: true, + minItems: 0 + }, + + messages: { + // eslint-disable-next-line eslint-plugin/report-message-format + restrictedVBind: '{{message}}' + } + }, + create(context) { + /** @type {ParsedOption[]} */ + const options = (context.options.length === 0 + ? DEFAULT_OPTIONS + : context.options + ).map(parseOption) + + return utils.defineTemplateBodyVisitor(context, { + /** + * @param {VDirectiveKey} node + */ + "VAttribute[directive=true][key.name.name='bind'] > VDirectiveKey"(node) { + for (const option of options) { + if (option.test(node)) { + const message = option.message || defaultMessage(node, option) + context.report({ + node, + messageId: 'restrictedVBind', + data: { message } + }) + return + } + } + } + }) + + /** + * @param {VDirectiveKey} key + * @param {ParsedOption} option + */ + function defaultMessage(key, option) { + const vbind = key.name.rawName === ':' ? '' : 'v-bind' + const arg = + key.argument != null && key.argument.type === 'VIdentifier' + ? `:${key.argument.rawName}` + : '' + const mod = option.modifiers.length + ? `.${option.modifiers.join('.')}` + : '' + let on = '' + if (option.useElement) { + on = ` on \`<${key.parent.parent.parent.rawName}>\`` + } + return `Using \`${vbind + arg + mod}\`${on} is not allowed.` + } + } +} diff --git a/tests/lib/rules/no-restricted-v-bind.js b/tests/lib/rules/no-restricted-v-bind.js new file mode 100644 index 000000000..6a7f0a447 --- /dev/null +++ b/tests/lib/rules/no-restricted-v-bind.js @@ -0,0 +1,153 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-restricted-v-bind') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2020 } +}) + +tester.run('no-restricted-v-bind', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: 'foo', modifiers: ['sync'] }] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: 'foo', element: 'input' }] + } + ], + invalid: [ + { + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive.', + line: 1, + column: 16 + } + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + 'Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive.' + ] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'], + errors: ['Using `:foo` is not allowed.'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo', 'bar'], + errors: ['Using `:foo` is not allowed.', 'Using `:bar` is not allowed.'] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: '/^(foo|bar)$/' }], + errors: ['Using `:foo` is not allowed.', 'Using `:bar` is not allowed.'] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: 'foo', modifiers: ['sync'] }], + errors: ['Using `:foo.sync` is not allowed.'] + }, + { + filename: 'test.vue', + code: + '', + options: ['/^v-/', { argument: 'foo', modifiers: ['sync'] }, null], + errors: [ + 'Using `:v-on` is not allowed.', + 'Using `:foo.sync` is not allowed.', + 'Using `v-bind` is not allowed.' + ] + }, + { + filename: 'test.vue', + code: ` + `, + options: ['/^v-/', { argument: 'foo', element: `/^My/` }], + errors: [ + 'Using `:v-on` is not allowed.', + 'Using `:foo` on `` is not allowed.' + ] + }, + { + filename: 'test.vue', + code: ` + `, + options: ['/^f/', { argument: 'foo' }], + errors: ['Using `:foo` is not allowed.'] + }, + { + filename: 'test.vue', + code: ` + `, + options: [{ argument: 'foo', message: 'foo' }], + errors: ['foo'] + } + ] +})