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']
+ }
+ ]
+})